@ljwei-stak/dsh-model-route-bot 0.66.3 → 0.67.0
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/.dsh-plugin/bot-product.mjs +81 -5
- package/.dsh-plugin/client.js +17490 -1791
- package/README.i18n.yaml +4 -4
- package/README.md +409 -364
- package/README.zh.md +409 -364
- package/apps/integration-client/README.md +31 -7
- package/apps/integration-client/api.mjs +14 -3
- package/apps/integration-client/binding-editor.jsx +308 -109
- package/apps/integration-client/binding-model.mjs +262 -0
- package/apps/integration-client/binding.css +144 -0
- package/apps/integration-client/bot-model-status.mjs +18 -0
- package/apps/integration-client/bot-profile-identity.jsx +197 -0
- package/apps/integration-client/bot-profile.css +57 -0
- package/apps/integration-client/bot-profile.mjs +97 -0
- package/apps/integration-client/bot-settings-dialog.css +18 -0
- package/apps/integration-client/bot-settings-dialog.jsx +111 -0
- package/apps/integration-client/bot-thread-header-bridge.css +20 -0
- package/apps/integration-client/bot-thread-header.jsx +278 -0
- package/apps/integration-client/bot-threads-panel.jsx +378 -0
- package/apps/integration-client/bot-threads.css +169 -0
- package/apps/integration-client/chat-design.mjs +250 -0
- package/apps/integration-client/chat-ui.jsx +175 -0
- package/apps/integration-client/chat.css +477 -0
- package/apps/integration-client/composer-draft-binding.mjs +50 -0
- package/apps/integration-client/composer-draft-bound.jsx +14 -0
- package/apps/integration-client/composer-draft-host.jsx +142 -0
- package/apps/integration-client/composer-draft-session.jsx +287 -0
- package/apps/integration-client/composer-draft-witness.mjs +115 -0
- package/apps/integration-client/composer-draft.css +4 -0
- package/apps/integration-client/composer-draft.mjs +322 -0
- package/apps/integration-client/contact-attention.jsx +54 -0
- package/apps/integration-client/data-panel.jsx +439 -0
- package/apps/integration-client/data.css +69 -0
- package/apps/integration-client/desktop-bridge.mjs +1 -0
- package/apps/integration-client/desktop-http-socket.mjs +49 -0
- package/apps/integration-client/desktop-keyboard.mjs +32 -0
- package/apps/integration-client/desktop-pane.jsx +99 -30
- package/apps/integration-client/desktop-stream.mjs +13 -3
- package/apps/integration-client/desktop-tools.jsx +19 -0
- package/apps/integration-client/desktop-touch.mjs +27 -0
- package/apps/integration-client/desktop.css +41 -0
- package/apps/integration-client/independent-chat-controller.mjs +224 -0
- package/apps/integration-client/independent-chat-entry.jsx +61 -0
- package/apps/integration-client/independent-chat-navigation.mjs +21 -0
- package/apps/integration-client/independent-chat-page.jsx +148 -0
- package/apps/integration-client/independent-chat-rich.jsx +64 -0
- package/apps/integration-client/independent-chat-rich.mjs +30 -0
- package/apps/integration-client/independent-chat.css +73 -0
- package/apps/integration-client/index.jsx +24 -16
- package/apps/integration-client/library-panel.jsx +4 -1
- package/apps/integration-client/licenses/jsqr-1.4.0-LICENSE.txt +202 -0
- package/apps/integration-client/main-route-editor.jsx +94 -0
- package/apps/integration-client/main-route-model.mjs +100 -0
- package/apps/integration-client/model-picker.css +37 -0
- package/apps/integration-client/model-picker.jsx +83 -0
- package/apps/integration-client/native-activity.css +141 -0
- package/apps/integration-client/native-activity.jsx +276 -0
- package/apps/integration-client/native-activity.mjs +384 -0
- package/apps/integration-client/notifications-panel.jsx +186 -0
- package/apps/integration-client/notifications.css +126 -0
- package/apps/integration-client/pairing-qr.jsx +79 -0
- package/apps/integration-client/panels.jsx +126 -115
- package/apps/integration-client/product-slots.jsx +46 -0
- package/apps/integration-client/routine-recorder.mjs +23 -2
- package/apps/integration-client/secure-pairing.jsx +84 -0
- package/apps/integration-client/tasks-panel.jsx +32 -10
- package/apps/integration-client/ui-icons.jsx +17 -0
- package/apps/integration-client/usage-explorer.jsx +43 -0
- package/apps/integration-client/usage-report.mjs +71 -0
- package/apps/integration-client/usage.css +34 -0
- package/apps/integration-client/web-panel-model.mjs +140 -0
- package/apps/integration-client/web-panel-page.jsx +221 -0
- package/apps/integration-client/web-panel-pins-live.jsx +28 -0
- package/apps/integration-client/web-panel.css +139 -0
- package/apps/integration-client/workspace-model.mjs +67 -0
- package/apps/integration-client/workspace-navigation.mjs +56 -0
- package/apps/integration-client/workspace-ui.jsx +173 -0
- package/apps/integration-client/workspace.css +332 -0
- package/modules/bot-runtime/SOURCE.json +176 -50
- package/modules/bot-runtime/index.mjs +169 -13
- package/modules/bot-runtime/product-overlay/OVERLAY.json +128 -2
- package/modules/bot-runtime/product-overlay/base/src/client/header.js +244 -0
- package/modules/bot-runtime/product-overlay/base/src/client/learning-card.js +202 -0
- package/modules/bot-runtime/product-overlay/base/src/client/model-menu.js +371 -0
- package/modules/bot-runtime/product-overlay/base/src/client/secret-card.js +363 -0
- package/modules/bot-runtime/product-overlay/base/src/client/sources.js +68 -0
- package/modules/bot-runtime/product-overlay/base/src/client/turn-error.js +260 -0
- package/modules/bot-runtime/product-overlay/base/src/host/channel.js +647 -0
- package/modules/bot-runtime/product-overlay/base/src/host/memory.js +2050 -0
- package/modules/bot-runtime/product-overlay/base/src/host/models.js +314 -0
- package/modules/bot-runtime/product-overlay/base/src/host/parts.js +1473 -0
- package/modules/bot-runtime/product-overlay/base/src/host/privacy.js +368 -0
- package/modules/bot-runtime/product-overlay/base/src/host/roster.js +776 -0
- package/modules/bot-runtime/product-overlay/base/src/host/store.js +512 -0
- package/modules/bot-runtime/product-overlay/base/src/host/usage.js +653 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/details-drawer.js.patch +48 -2
- package/modules/bot-runtime/product-overlay/patches/src/client/header.js.patch +41 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/index.js.patch +33 -1
- package/modules/bot-runtime/product-overlay/patches/src/client/learning-card.js.patch +20 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/model-menu.js.patch +24 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/secret-card.js.patch +25 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/sources.js.patch +52 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/turn-error.js.patch +23 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/channel.js.patch +46 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/memory.js.patch +11 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/models.js.patch +16 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/parts.js.patch +95 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/privacy.js.patch +11 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/roster.js.patch +145 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/store.js.patch +68 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/usage.js.patch +11 -0
- package/modules/bot-runtime/read-model.mjs +19 -4
- package/modules/bot-runtime/vendor/ds-bot/src/client/details-drawer.js +15 -5
- package/modules/bot-runtime/vendor/ds-bot/src/client/header.js +9 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/index.js +9 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/learning-card.js +2 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/model-menu.js +5 -3
- package/modules/bot-runtime/vendor/ds-bot/src/client/secret-card.js +3 -3
- package/modules/bot-runtime/vendor/ds-bot/src/client/sources.js +25 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/turn-error.js +3 -2
- package/modules/bot-runtime/vendor/ds-bot/src/host/channel.js +14 -2
- package/modules/bot-runtime/vendor/ds-bot/src/host/memory.js +1 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/models.js +7 -0
- package/modules/bot-runtime/vendor/ds-bot/src/host/parts.js +26 -9
- package/modules/bot-runtime/vendor/ds-bot/src/host/privacy.js +1 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/roster.js +112 -3
- package/modules/bot-runtime/vendor/ds-bot/src/host/store.js +34 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/usage.js +1 -1
- package/modules/independent-runtime/index.mjs +152 -62
- package/modules/independent-runtime/remote-tools.mjs +8 -5
- package/modules/openmaus-protocol/NOTICE +10 -0
- package/modules/openmaus-protocol/README.md +15 -0
- package/modules/openmaus-protocol/SOURCE.json +68 -0
- package/modules/openmaus-protocol/dist/protocol.mjs +1203 -0
- package/modules/openmaus-protocol/index.mjs +1 -0
- package/modules/openmaus-protocol/transport.mjs +77 -0
- package/modules/openmaus-protocol/vendor/LICENSE +202 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/README.md +405 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/credentials.ts +128 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/descriptor.ts +84 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/noise.ts +456 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/phone.ts +104 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/session.ts +417 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.json +2565 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.sha256 +1 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/yamux.ts +335 -0
- package/package.json +20 -6
- package/packages/bot-acp/README.md +156 -0
- package/packages/bot-acp/licenses/LICENSE-OpenMausBot +202 -0
- package/packages/bot-acp/licenses/NOTICE.md +27 -0
- package/packages/bot-acp/src/acp-adapter.mjs +795 -0
- package/packages/bot-acp/src/acp-host.mjs +148 -0
- package/packages/bot-acp/src/native-launch.mjs +121 -0
- package/packages/bot-acp/src/permission-policy.mjs +283 -0
- package/packages/bot-acp/src/rpc-codec.mjs +265 -0
- package/packages/bot-acp/src/session-core.mjs +581 -0
- package/packages/bot-acp/src/stdio-rpc.mjs +329 -0
- package/packages/bot-engines/README.md +281 -0
- package/packages/bot-engines/licenses/LICENSE-OpenMausBot +202 -0
- package/packages/bot-engines/licenses/NOTICE.md +32 -0
- package/packages/bot-engines/src/claude-engine.mjs +772 -0
- package/packages/bot-engines/src/claude-llm-adapter.mjs +797 -0
- package/packages/bot-engines/src/claude-llm-turn.mjs +292 -0
- package/packages/bot-engines/src/claude-permission-broker.mjs +130 -0
- package/packages/bot-engines/src/claude-permission-proxy.mjs +87 -0
- package/packages/bot-engines/src/claude-protocol.mjs +177 -0
- package/packages/bot-engines/src/driver-catalog.mjs +334 -0
- package/packages/bot-engines/src/host-loop-request.mjs +95 -0
- package/packages/bot-engines/src/host-registrations.mjs +105 -0
- package/packages/capability-broker/src/cli-bootstrap.mjs +86 -16
- package/packages/capability-broker/src/cli-bridge-response.mjs +80 -0
- package/packages/capability-broker/src/cli-stdio-bridge.mjs +21 -8
- package/packages/cli-runtime/src/codex-session-core.mjs +4 -0
- package/packages/cli-runtime/src/native-cli-output.mjs +57 -0
- package/packages/cli-runtime/src/native-peer.ps1 +51 -3
- package/packages/cli-runtime/src/native-process.mjs +77 -14
- package/packages/model-router-plugin/README.md +3 -3
- package/packages/model-router-plugin/package.json +1 -1
- package/packages/orchestration/src/binding-product.mjs +19 -0
- package/packages/orchestration/src/bot-data-actions.mjs +45 -0
- package/packages/orchestration/src/bot-data-chart.mjs +109 -0
- package/packages/orchestration/src/bot-data-context.mjs +44 -0
- package/packages/orchestration/src/bot-data-engine.mjs +809 -0
- package/packages/orchestration/src/bot-data-io.mjs +186 -0
- package/packages/orchestration/src/bot-data-sql.mjs +195 -0
- package/packages/orchestration/src/bot-data-surface.mjs +144 -0
- package/packages/orchestration/src/bot-data-tools.mjs +68 -0
- package/packages/orchestration/src/bot-data-worker.mjs +228 -0
- package/packages/orchestration/src/bot-independent-execution.mjs +195 -0
- package/packages/orchestration/src/bot-independent-stream.mjs +74 -0
- package/packages/orchestration/src/bot-loop.mjs +152 -19
- package/packages/orchestration/src/bot-notifications.mjs +159 -0
- package/packages/orchestration/src/bot-policy.mjs +43 -4
- package/packages/orchestration/src/bot-profile.mjs +171 -0
- package/packages/orchestration/src/bot-routine.mjs +4 -4
- package/packages/orchestration/src/bot-runtime-assembly.mjs +255 -8
- package/packages/orchestration/src/bot-runtime-switch.mjs +13 -2
- package/packages/orchestration/src/bot-serving.mjs +164 -0
- package/packages/orchestration/src/bot-session-store.mjs +105 -9
- package/packages/orchestration/src/bot-settings.mjs +50 -11
- package/packages/orchestration/src/bot-thread-admission.mjs +84 -0
- package/packages/orchestration/src/bot-thread-host.mjs +187 -0
- package/packages/orchestration/src/bot-threads.mjs +790 -0
- package/packages/orchestration/src/data-product-access.mjs +37 -0
- package/packages/orchestration/src/data-tool-contract.mjs +291 -0
- package/packages/orchestration/src/desktop-http-carrier.mjs +120 -0
- package/packages/orchestration/src/desktop-proxy.mjs +311 -0
- package/packages/orchestration/src/desktop-sessions.mjs +43 -9
- package/packages/orchestration/src/ds-lifecycle.mjs +31 -4
- package/packages/orchestration/src/grant-refresh-queue.mjs +17 -0
- package/packages/orchestration/src/independent-chat-page.mjs +39 -0
- package/packages/orchestration/src/independent-chat-service.mjs +308 -0
- package/packages/orchestration/src/independent-context-state.mjs +44 -0
- package/packages/orchestration/src/independent-message-wire.mjs +84 -0
- package/packages/orchestration/src/independent-tool-catalog.mjs +175 -0
- package/packages/orchestration/src/independent-tool-families.mjs +73 -0
- package/packages/orchestration/src/independent-tool-host.mjs +344 -0
- package/packages/orchestration/src/main-bot-grant.mjs +15 -0
- package/packages/orchestration/src/main-model-routes.mjs +208 -0
- package/packages/orchestration/src/model-accounting.mjs +99 -35
- package/packages/orchestration/src/model-auxiliary-dispatch.mjs +187 -0
- package/packages/orchestration/src/model-loop-frame.mjs +179 -0
- package/packages/orchestration/src/model-prepared-generation.mjs +175 -0
- package/packages/orchestration/src/model-request-bounds.mjs +111 -0
- package/packages/orchestration/src/native-activity-contract.mjs +491 -0
- package/packages/orchestration/src/native-activity-service.mjs +102 -0
- package/packages/orchestration/src/native-activity-store.mjs +249 -0
- package/packages/orchestration/src/native-conversation-tools.mjs +78 -0
- package/packages/orchestration/src/native-data-tools.mjs +100 -0
- package/packages/orchestration/src/native-remote-actions.mjs +742 -0
- package/packages/orchestration/src/native-team-tools.mjs +73 -0
- package/packages/orchestration/src/pairing-invite.mjs +37 -0
- package/packages/orchestration/src/product-composition.mjs +1559 -1001
- package/packages/orchestration/src/remote-machines.mjs +82 -14
- package/packages/orchestration/src/routine-execution.mjs +5 -3
- package/packages/orchestration/src/routine-session.mjs +19 -13
- package/packages/orchestration/src/secure-pairing-host.mjs +112 -0
- package/packages/orchestration/src/task-journal.mjs +29 -0
- package/packages/orchestration/src/task-product.mjs +17 -0
- package/packages/orchestration/src/team-assignment-store.mjs +120 -0
- package/packages/orchestration/src/team-bridge.mjs +282 -0
- package/packages/orchestration/src/team-scheduler.mjs +83 -0
- package/packages/orchestration/src/team-sdk-tools.mjs +84 -0
- package/packages/orchestration/src/team-tool-contract.mjs +284 -0
- package/packages/orchestration/src/ui-host-namespace.mjs +23 -0
- package/packages/orchestration/src/web-panel-actions.mjs +103 -0
- package/packages/orchestration/src/web-panel-framing.mjs +227 -0
- package/packages/orchestration/src/web-panel-store.mjs +206 -0
- package/remote-computer-agent/README.md +39 -29
- package/remote-computer-agent/build.mjs +3 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_LICENSE +202 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_NOTICE +10 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_SOURCE.json +68 -0
- package/remote-computer-agent/dist/cli-stdio-bridge.mjs +215 -25
- package/remote-computer-agent/dist/cli-stdio-bridge.mjs.map +4 -4
- package/remote-computer-agent/dist/cli.mjs +1688 -168
- package/remote-computer-agent/dist/cli.mjs.map +4 -4
- package/remote-computer-agent/dist/index.mjs +1579 -122
- package/remote-computer-agent/dist/index.mjs.map +4 -4
- package/remote-computer-agent/dist/native-peer.ps1 +51 -3
- package/remote-computer-agent/package-lock.json +308 -2
- package/remote-computer-agent/package.json +2 -1
- package/remote-computer-agent/pair-device.ps1 +20 -0
- package/remote-computer-agent/setup.ps1 +38 -16
- package/remote-computer-agent/src/cli.ts +24 -3
- package/remote-computer-agent/src/desktop-session.ts +11 -3
- package/remote-computer-agent/src/e2e-server.ts +81 -0
- package/remote-computer-agent/src/pairing-page.ts +35 -0
- package/remote-computer-agent/src/protocol.ts +25 -25
- package/remote-computer-agent/src/secure-pairing.ts +105 -0
- package/remote-computer-agent/src/store.ts +20 -5
- package/remote-computer-agent/src/transport.ts +39 -15
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure state for the Bot binding editor.
|
|
3
|
+
*
|
|
4
|
+
* The Host's committed catalog is the only source of providers, models, machines,
|
|
5
|
+
* capabilities, official tools and funding scopes: nothing here invents a route,
|
|
6
|
+
* a workspace, a capability or an execution mode the Host never offered. A blank
|
|
7
|
+
* number stays blank (null), never 0, so "0 表示禁止付费" keeps its meaning and an
|
|
8
|
+
* unfinished field is never silently completed with a default.
|
|
9
|
+
*
|
|
10
|
+
* No React and no DOM, so the integration specs exercise the same rules the
|
|
11
|
+
* component renders.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const BOT_BINDING_SCHEMA = 1
|
|
15
|
+
|
|
16
|
+
/** Execution modes the stored schema accepts, least privileged first. */
|
|
17
|
+
export const EXECUTION_MODES = Object.freeze(['read-only', 'workspace-write', 'cli-native'])
|
|
18
|
+
export const MODE_LABEL = Object.freeze({
|
|
19
|
+
'read-only': '只读执行',
|
|
20
|
+
'workspace-write': '可写工作区',
|
|
21
|
+
'cli-native': 'CLI 自身权限',
|
|
22
|
+
})
|
|
23
|
+
export const MODE_HELP = Object.freeze({
|
|
24
|
+
'read-only': 'CLI 只能读取已授权内容,不写工作区。',
|
|
25
|
+
'workspace-write': 'CLI 可写入此 Bot 已授权的工作区。',
|
|
26
|
+
'cli-native': '沿用 CLI 自己的权限设置,宿主不再额外收紧。',
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
export const FUNDING_MODES = Object.freeze([
|
|
30
|
+
{ value: 'api', label: 'API(按金额)', help: '按 Token 计价,需要单价、请求上界与每日/每月预算。' },
|
|
31
|
+
{ value: 'subscription', label: '订阅(按次数)', help: '按调用次数计数,不会自动回退到 API。' },
|
|
32
|
+
])
|
|
33
|
+
export const ROUTING_PRESETS = Object.freeze([
|
|
34
|
+
{ value: '', label: '沿用全局预设' },
|
|
35
|
+
{ value: 'economy', label: '省钱优先' },
|
|
36
|
+
{ value: 'balanced', label: '均衡' },
|
|
37
|
+
{ value: 'quality', label: '效果优先' },
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
export const routeOf = model => (model && model.provider && model.model ? `${model.provider}/${model.model}` : '')
|
|
41
|
+
|
|
42
|
+
export const splitRoute = route => {
|
|
43
|
+
const text = typeof route === 'string' ? route : ''
|
|
44
|
+
const at = text.indexOf('/')
|
|
45
|
+
return at < 0 ? { provider: '', model: '' } : { provider: text.slice(0, at), model: text.slice(at + 1) }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const findModel = (catalog, route) => (catalog?.models ?? []).find(item => routeOf(item) === route) ?? null
|
|
49
|
+
|
|
50
|
+
/** Providers with their exact routes, in the order the Host listed them. */
|
|
51
|
+
export function providerGroups(catalog) {
|
|
52
|
+
const groups = new Map()
|
|
53
|
+
for (const item of catalog?.models ?? []) {
|
|
54
|
+
if (!item?.provider) continue
|
|
55
|
+
if (!groups.has(item.provider)) groups.set(item.provider, { provider: item.provider, models: [] })
|
|
56
|
+
groups.get(item.provider).models.push({
|
|
57
|
+
route: routeOf(item), model: item.model, label: item.label ?? item.model,
|
|
58
|
+
cliModels: [...(item.cliModels ?? [])],
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
return [...groups.values()]
|
|
62
|
+
.map(group => ({ ...group, models: group.models.sort((left, right) => left.route.localeCompare(right.route)) }))
|
|
63
|
+
.sort((left, right) => left.provider.localeCompare(right.provider))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A stored blank stays blank; a stored 0 stays "0". */
|
|
67
|
+
export const textOf = value => (value === null || value === undefined ? '' : String(value))
|
|
68
|
+
|
|
69
|
+
export const numberOrNull = text => {
|
|
70
|
+
if (text === null || text === undefined) return null
|
|
71
|
+
const raw = String(text).trim()
|
|
72
|
+
if (raw === '') return null
|
|
73
|
+
const value = Number(raw)
|
|
74
|
+
return Number.isFinite(value) ? value : null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function workspaceOptions(catalog) {
|
|
78
|
+
return (catalog?.machines ?? []).map(machine => ({
|
|
79
|
+
machineId: machine.machineId, label: machine.label ?? machine.machineId,
|
|
80
|
+
location: machine.location, status: machine.status ?? 'unknown',
|
|
81
|
+
roots: (machine.roots ?? []).map(root => ({ rootId: root.rootId, label: root.label ?? root.rootId, modes: [...(root.modes ?? [])] })),
|
|
82
|
+
}))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const workspaceKey = (machineId, rootId) => `${machineId ?? ''}/${rootId ?? ''}`
|
|
86
|
+
|
|
87
|
+
export const accountFor = (accounts, provider) => (accounts?.items ?? []).find(item => item.provider === provider) ?? null
|
|
88
|
+
|
|
89
|
+
/** A funding scope only covers the modes and routes the Host published for it. */
|
|
90
|
+
export function scopeAllows(scope, mode, route) {
|
|
91
|
+
if (!scope) return false
|
|
92
|
+
if (Array.isArray(scope.modes) && scope.modes.length > 0 && !scope.modes.includes(mode)) return false
|
|
93
|
+
if (Array.isArray(scope.models) && scope.models.length > 0 && route && !scope.models.some(item => routeOf(item) === route)) return false
|
|
94
|
+
return true
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* One card per official tool the Host offers. `identities` are the CLI models the
|
|
99
|
+
* Host verified for the currently selected API model; a grant whose cliModel is
|
|
100
|
+
* not in that list is stale and the Host would refuse it, so it is shown instead
|
|
101
|
+
* of being dropped or quietly re-pointed at another model.
|
|
102
|
+
*/
|
|
103
|
+
export function toolCards({ catalog, model, grants }) {
|
|
104
|
+
const verified = model?.cliModels ?? []
|
|
105
|
+
return (catalog?.tools ?? []).map(tool => {
|
|
106
|
+
const identities = verified.filter(item => item.toolId === tool.toolId)
|
|
107
|
+
const grant = (grants ?? []).find(item => item.toolId === tool.toolId) ?? null
|
|
108
|
+
const modes = (tool.modes ?? []).filter(mode => EXECUTION_MODES.includes(mode))
|
|
109
|
+
return {
|
|
110
|
+
toolId: tool.toolId, label: tool.label ?? tool.toolId, vendor: tool.vendor ?? '', source: tool.source ?? '',
|
|
111
|
+
modes, grantTools: [...(tool.grantTools ?? [])], identities, grant,
|
|
112
|
+
stale: Boolean(grant) && !identities.some(item => item.model === grant.cliModel),
|
|
113
|
+
}
|
|
114
|
+
}).sort((left, right) => left.toolId.localeCompare(right.toolId))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The least privileged mode the Host offers for a tool: a grant never starts wide. */
|
|
118
|
+
export function defaultMode(modes) {
|
|
119
|
+
const offered = (modes ?? []).filter(mode => EXECUTION_MODES.includes(mode))
|
|
120
|
+
return offered.includes('read-only') ? 'read-only' : offered[0] ?? 'read-only'
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function seedForm(record, accounts) {
|
|
124
|
+
const settings = record?.settings ?? {}, catalog = record?.catalog ?? {}
|
|
125
|
+
const groups = providerGroups(catalog)
|
|
126
|
+
const fundingAccount = accountFor(accounts, settings.model?.provider)
|
|
127
|
+
const grants = settings.machineGrants ?? []
|
|
128
|
+
const workspaceRef = settings.workspaceRef ?? grants[0] ?? null
|
|
129
|
+
const machineId = workspaceRef?.machineId ?? '', rootId = workspaceRef?.rootId ?? ''
|
|
130
|
+
const grant = grants.find(item => item.machineId === machineId && item.rootId === rootId) ?? null
|
|
131
|
+
return {
|
|
132
|
+
provider: settings.model?.provider ?? (groups.length === 1 ? groups[0].provider : ''),
|
|
133
|
+
model: routeOf(settings.model), enabled: settings.enabled === true,
|
|
134
|
+
workspace: machineId && rootId ? workspaceKey(machineId, rootId) : '',
|
|
135
|
+
workspaceMode: grant?.mode ?? 'read-only',
|
|
136
|
+
capabilities: [...(settings.capabilities ?? [])],
|
|
137
|
+
officialTools: (settings.officialTools ?? []).map(tool => ({ ...tool })),
|
|
138
|
+
fundingMode: settings.funding?.mode ?? 'api',
|
|
139
|
+
input: textOf(settings.funding?.pricing?.input), output: textOf(settings.funding?.pricing?.output),
|
|
140
|
+
...(settings.mainRoutes ? { mainRoutes: structuredClone(settings.mainRoutes) } : {}),
|
|
141
|
+
maxInputTokens: textOf(settings.funding?.request?.maxInputTokens), maxOutputTokens: textOf(settings.funding?.request?.maxOutputTokens),
|
|
142
|
+
daily: textOf(settings.funding?.dailyBudgetUsd), monthly: textOf(settings.funding?.monthlyBudgetUsd),
|
|
143
|
+
dailyCalls: textOf(settings.funding?.subscription?.dailyCalls), monthlyCalls: textOf(settings.funding?.subscription?.monthlyCalls),
|
|
144
|
+
maxConcurrentTasks: textOf(settings.maxConcurrentTasks),
|
|
145
|
+
// The funding scope's own limits, not an invented default.
|
|
146
|
+
accountDaily: textOf(fundingAccount?.dailyLimitUsd), accountMonthly: textOf(fundingAccount?.monthlyLimitUsd),
|
|
147
|
+
routing: {
|
|
148
|
+
preset: settings.routing?.preset ?? '',
|
|
149
|
+
dailyBudgetUsd: textOf(settings.routing?.dailyBudgetUsd),
|
|
150
|
+
monthlyBudgetUsd: textOf(settings.routing?.monthlyBudgetUsd),
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Build the stored payload. A blank numeric field is sent as null, never 0, and a
|
|
157
|
+
* half-filled group is sent as null rather than as an object holding nulls the
|
|
158
|
+
* stored schema would reject.
|
|
159
|
+
*/
|
|
160
|
+
export function buildBinding({ form, model, account, machine, root }) {
|
|
161
|
+
const funding = { mode: form.fundingMode, scopeId: account?.enabled ? account.id : null,
|
|
162
|
+
pricing: null, request: null, dailyBudgetUsd: null, monthlyBudgetUsd: null, subscription: null }
|
|
163
|
+
if (form.fundingMode === 'api') {
|
|
164
|
+
const input = numberOrNull(form.input), output = numberOrNull(form.output)
|
|
165
|
+
funding.pricing = input === null || output === null ? null : { input, output }
|
|
166
|
+
const maxInputTokens = numberOrNull(form.maxInputTokens), maxOutputTokens = numberOrNull(form.maxOutputTokens)
|
|
167
|
+
funding.request = maxInputTokens === null || maxOutputTokens === null ? null : { maxInputTokens, maxOutputTokens }
|
|
168
|
+
funding.dailyBudgetUsd = numberOrNull(form.daily)
|
|
169
|
+
funding.monthlyBudgetUsd = numberOrNull(form.monthly)
|
|
170
|
+
} else {
|
|
171
|
+
const dailyCalls = numberOrNull(form.dailyCalls), monthlyCalls = numberOrNull(form.monthlyCalls)
|
|
172
|
+
funding.subscription = dailyCalls === null || monthlyCalls === null ? null : { dailyCalls, monthlyCalls }
|
|
173
|
+
}
|
|
174
|
+
const routing = {
|
|
175
|
+
preset: form.routing.preset === '' ? null : form.routing.preset,
|
|
176
|
+
dailyBudgetUsd: numberOrNull(form.routing.dailyBudgetUsd),
|
|
177
|
+
monthlyBudgetUsd: numberOrNull(form.routing.monthlyBudgetUsd),
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
schemaVersion: BOT_BINDING_SCHEMA,
|
|
181
|
+
enabled: form.enabled === true,
|
|
182
|
+
model: model ? { provider: model.provider, model: model.model } : null,
|
|
183
|
+
officialTools: (form.officialTools ?? []).map(tool => ({ toolId: tool.toolId, cliModel: tool.cliModel, mode: tool.mode }))
|
|
184
|
+
.sort((left, right) => left.toolId.localeCompare(right.toolId)),
|
|
185
|
+
machineGrants: machine && root ? [{ machineId: machine.machineId, rootId: root.rootId, mode: form.workspaceMode, capabilities: [] }] : [],
|
|
186
|
+
capabilities: [...(form.capabilities ?? [])].sort(),
|
|
187
|
+
workspaceRef: machine && root ? { kind: machine.location ?? 'local', machineId: machine.machineId, rootId: root.rootId } : null,
|
|
188
|
+
funding,
|
|
189
|
+
maxConcurrentTasks: numberOrNull(form.maxConcurrentTasks),
|
|
190
|
+
routing: routing.preset === null && routing.dailyBudgetUsd === null && routing.monthlyBudgetUsd === null ? null : routing,
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const apiFields = [
|
|
195
|
+
['input', '输入价格(USD / 百万 Token)'], ['output', '输出价格(USD / 百万 Token)'],
|
|
196
|
+
['maxInputTokens', '最大输入 Token'], ['maxOutputTokens', '最大输出 Token'],
|
|
197
|
+
['daily', 'Bot 每日预算(USD)'], ['monthly', 'Bot 每月预算(USD)'],
|
|
198
|
+
]
|
|
199
|
+
const subscriptionFields = [['dailyCalls', '每日订阅调用次数'], ['monthlyCalls', '每月订阅调用次数']]
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* What this window can already see as missing, mirrored from the Host's own
|
|
203
|
+
* admission rules. The Host still validates every save; this only refuses a call
|
|
204
|
+
* that could not succeed, and never widens what is sent.
|
|
205
|
+
*/
|
|
206
|
+
export function bindingProblems({ form, model, account, machine, root, catalog, scope, mainRoutes = false }) {
|
|
207
|
+
const problems = []
|
|
208
|
+
// The stored schema needs a usable concurrency even for a disabled binding.
|
|
209
|
+
const concurrency = numberOrNull(form.maxConcurrentTasks)
|
|
210
|
+
if (concurrency === null || !Number.isInteger(concurrency) || concurrency < 1 || concurrency > 32) problems.push('最大并发任务数必须是 1 至 32 的整数。')
|
|
211
|
+
if (form.enabled !== true) return problems
|
|
212
|
+
if (!model) problems.push('请选择目录中的准确模型;留空表示此绑定保持停用。')
|
|
213
|
+
if (!mainRoutes && model?.fundingModes && !model.fundingModes.includes(form.fundingMode)) problems.push('此模型不支持所选资金渠道,请选择宿主列出的渠道。')
|
|
214
|
+
if (!machine || !root) problems.push('请选择已登记的工作区(机器与根目录)。')
|
|
215
|
+
else if (!root.modes.includes(form.workspaceMode)) problems.push(`所选根目录不再支持${form.workspaceMode === 'read-only' ? '只读' : '写入'}模式,请重新选择。`)
|
|
216
|
+
if (!mainRoutes) {
|
|
217
|
+
if (!account) problems.push('宿主目录里没有这个提供商的资金范围,请刷新或改选模型。')
|
|
218
|
+
else if (!account.enabled) problems.push(`${account.provider} 资金范围尚未启用;它不是厂商余额,需要本地预算上限。`)
|
|
219
|
+
else if (!scopeAllows(scope, form.fundingMode, routeOf(model))) problems.push('资金范围不支持所选渠道或准确模型,请重新选择。')
|
|
220
|
+
for (const [key, label] of form.fundingMode === 'api' ? apiFields : subscriptionFields) {
|
|
221
|
+
if (numberOrNull(form[key]) === null) problems.push(`${label}尚未填写;0 表示禁止付费/调用,空值表示未配置。`)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
for (const id of form.capabilities ?? []) {
|
|
225
|
+
if (!(catalog?.capabilities ?? []).some(item => item.id === id)) problems.push(`能力 ${id} 已不在宿主目录,请取消勾选。`)
|
|
226
|
+
}
|
|
227
|
+
for (const tool of form.officialTools ?? []) {
|
|
228
|
+
if (!(catalog?.tools ?? []).some(item => item.toolId === tool.toolId && (item.modes ?? []).includes(tool.mode))) {
|
|
229
|
+
problems.push(`${tool.toolId} 尚未获准使用该执行模式,请移除或改选宿主提供的模式。`)
|
|
230
|
+
} else if (!(model?.cliModels ?? []).some(item => item.toolId === tool.toolId && item.model === tool.cliModel)) {
|
|
231
|
+
problems.push(`${tool.toolId} / ${tool.cliModel} 与当前模型的 CLI 身份尚未核验,请移除后重新授予。`)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return problems
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const FAILURES = {
|
|
238
|
+
BOT_SETTINGS_CONFLICT: '绑定已在另一窗口修改。请刷新后按最新配置重新提交。',
|
|
239
|
+
MAIN_ROUTES_RELOAD_REQUIRED: 'Main 路线配置已更新。请载入宿主最新设置后再保存。',
|
|
240
|
+
MAIN_ROUTE_FUNDING_UNAVAILABLE: '宿主无法核验这条路线的模型、资金范围或限额。请重新检查路线配置。',
|
|
241
|
+
BOT_SETTINGS_SCOPE_DENIED: '宿主拒绝了这次授权:所选模型、工具、模式或工作区已不在有效目录内。',
|
|
242
|
+
BOT_SETTINGS_INCOMPLETE: '宿主认为绑定还不完整:模型、工作区、资金范围或预算缺失。',
|
|
243
|
+
BOT_SETTINGS_UNKNOWN_TOOL: '官方工具必须来自宿主固定注册表。',
|
|
244
|
+
BOT_SETTINGS_RETIRED: '此 Bot 已退休,身份不能重新授予。',
|
|
245
|
+
BOT_SETTINGS_UNKNOWN_BOT: '此 Bot 不在已提交的宿主目录,请刷新。',
|
|
246
|
+
REMOTE_OWNER_REQUIRED: '请以已认证的宿主拥有者身份保存 Bot 绑定。',
|
|
247
|
+
BOT_SETTINGS_PUBLISH_FAILED: '绑定已提交,但宿主授权同步失败;此 Bot 已暂停,请刷新。',
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Host failures are shown as the Host words them, never as a success. */
|
|
251
|
+
export const failureText = error => {
|
|
252
|
+
const code = error?.code
|
|
253
|
+
const message = error?.message ? String(error.message) : ''
|
|
254
|
+
// A mapped code keeps the Host's own sentence: it names the exact route, tool or
|
|
255
|
+
// mode that was refused, which a generic sentence would hide.
|
|
256
|
+
if (code && FAILURES[code]) return message ? `${FAILURES[code]}(宿主:${message})` : FAILURES[code]
|
|
257
|
+
return message || '保存失败,宿主没有返回原因。'
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export const workspaceLabel = (machine, root) => `${machine?.label ?? machine?.machineId ?? '未选择'} / ${root?.label ?? root?.rootId ?? '未选择'}`
|
|
261
|
+
|
|
262
|
+
export const toolSourceLabel = source => source || '厂商官方 CLI'
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/* Bot binding editor: a Bot-centred, stepped model picker plus one independent
|
|
2
|
+
official-tools module. Tokens come from the product stylesheet, so light and
|
|
3
|
+
dark themes and the 375px layout follow the rest of the workspace. */
|
|
4
|
+
|
|
5
|
+
/* The shared muted token sits at 4.39:1 on the light page background, just under AA
|
|
6
|
+
for 12px text; this local token keeps the same grey family above it. */
|
|
7
|
+
.bb-root { --bb-muted: #6b6b73; display: flex; flex-direction: column; gap: 18px; min-width: 0; }
|
|
8
|
+
html body[data-bp-appearance="dark"] .bb-root,
|
|
9
|
+
html body[data-ds-dark-theme]:not([data-bp-appearance="light"]) .bb-root { --bb-muted: #a8a8b0; }
|
|
10
|
+
.bb-head { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
|
|
11
|
+
.bb-eyebrow { margin: 0; color: var(--bb-muted, #6b6b73); font-size: 11px; font-weight: 500; letter-spacing: .04em; }
|
|
12
|
+
.bb-title { margin: 0; font-size: 15px; font-weight: 600; letter-spacing: -.01em; color: var(--bp-ink, #242427); }
|
|
13
|
+
.bb-sub { margin: 0; color: var(--bb-muted, #6b6b73); font-size: 12px; line-height: 1.6; }
|
|
14
|
+
.bb-summary { display: flex; flex-wrap: wrap; gap: 6px 18px; margin: 10px 0 0; padding: 10px 12px; border: 1px solid var(--bp-border, #e6e6e9); border-radius: 9px; background: var(--bp-subtle, #f6f6f7); }
|
|
15
|
+
.bb-summary div { display: flex; align-items: baseline; gap: 6px; min-width: 0; font-size: 12px; }
|
|
16
|
+
.bb-summary dt { color: var(--bb-muted, #6b6b73); font-size: 11px; white-space: nowrap; }
|
|
17
|
+
.bb-summary dd { margin: 0; color: var(--bp-ink, #242427); overflow-wrap: anywhere; }
|
|
18
|
+
|
|
19
|
+
.bb-steps { display: flex; flex-direction: column; gap: 12px; margin: 0; padding: 0; list-style: none; counter-reset: none; }
|
|
20
|
+
.bb-step { border: 1px solid var(--bp-border, #e6e6e9); border-radius: 11px; background: var(--bp-surface, #fff); min-width: 0; }
|
|
21
|
+
.bb-step[data-state=done] { border-color: color-mix(in srgb, var(--bp-focus, #617b9f) 42%, var(--bp-border, #e6e6e9)); }
|
|
22
|
+
.bb-step[data-state=blocked] { border-color: color-mix(in srgb, var(--bp-warning, #8b6526) 45%, var(--bp-border, #e6e6e9)); }
|
|
23
|
+
.bb-step-head { display: flex; align-items: flex-start; gap: 11px; padding: 13px 15px 0; }
|
|
24
|
+
.bb-step-index { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; flex: none; margin-top: 1px; border: 1px solid var(--bp-border, #e6e6e9); border-radius: 7px; background: var(--bp-subtle, #f6f6f7); color: var(--bb-muted, #6b6b73); font-size: 11px; font-weight: 550; }
|
|
25
|
+
.bb-step[data-state=done] .bb-step-index { color: var(--bp-on-solid, #fff); background: var(--bp-solid, #242427); border-color: var(--bp-solid, #242427); }
|
|
26
|
+
.bb-step-head strong { display: block; font-size: 13px; font-weight: 550; color: var(--bp-ink, #242427); }
|
|
27
|
+
.bb-step-head p { margin: 3px 0 0; color: var(--bb-muted, #6b6b73); font-size: 12px; line-height: 1.55; }
|
|
28
|
+
.bb-step-state { margin-left: auto; flex: none; }
|
|
29
|
+
.bb-step-body { display: flex; flex-direction: column; gap: 10px; padding: 12px 15px 15px; }
|
|
30
|
+
|
|
31
|
+
.bb-search { display: flex; align-items: center; gap: 8px; min-height: 33px; padding: 0 10px; border: 1px solid var(--bp-border, #e6e6e9); border-radius: 8px; background: var(--bp-subtle, #f6f6f7); color: var(--bb-muted, #6b6b73); }
|
|
32
|
+
.bb-search:focus-within { border-color: var(--bp-focus, #617b9f); }
|
|
33
|
+
.bb-search input { flex: 1; min-width: 0; border: 0; background: none; color: var(--bp-ink, #242427); font-size: 13px; outline: none; }
|
|
34
|
+
.bb-search .bb-icon { display: inline-flex; flex: none; }
|
|
35
|
+
|
|
36
|
+
.bb-list { display: flex; flex-direction: column; gap: 6px; margin: 0; padding: 0; list-style: none; }
|
|
37
|
+
.bb-list-scroll { max-height: 268px; overflow-y: auto; overscroll-behavior: contain; padding-right: 2px; }
|
|
38
|
+
.bb-option { display: flex; align-items: center; gap: 10px; width: 100%; padding: 8px 11px; border: 1px solid var(--bp-border, #e6e6e9); border-radius: 9px; background: var(--bp-surface, #fff); color: var(--bp-ink, #242427); font-size: 13px; text-align: left; cursor: pointer; transition: background-color .14s, border-color .14s; }
|
|
39
|
+
.bb-option:hover { background: var(--bp-subtle, #f6f6f7); border-color: var(--bp-quiet, #a1a1a9); }
|
|
40
|
+
.bb-option[aria-pressed=true] { border-color: var(--bp-solid, #242427); background: var(--bp-hover, #ededf0); font-weight: 500; }
|
|
41
|
+
.bb-option:disabled { opacity: .5; cursor: default; }
|
|
42
|
+
.bb-option-inline { width: auto; flex: none; }
|
|
43
|
+
.bb-option-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
|
|
44
|
+
.bb-option-text strong { font-size: 13px; font-weight: 500; overflow-wrap: anywhere; }
|
|
45
|
+
.bb-option-text small { color: var(--bb-muted, #6b6b73); font-size: 11px; }
|
|
46
|
+
.bb-option-mark { display: inline-flex; align-items: center; gap: 5px; flex: none; color: var(--bb-muted, #6b6b73); font-size: 11px; }
|
|
47
|
+
|
|
48
|
+
.bb-pill { display: inline-flex; align-items: center; gap: 5px; padding: 2px 8px; border: 1px solid var(--bp-border, #e6e6e9); border-radius: 999px; background: var(--bp-subtle, #f6f6f7); color: var(--bb-muted, #6b6b73); font-size: 11px; line-height: 1.5; white-space: nowrap; }
|
|
49
|
+
.bb-pill[data-tone=ok] { color: #3f7a54; border-color: color-mix(in srgb, #3f7a54 30%, transparent); background: color-mix(in srgb, #3f7a54 10%, transparent); }
|
|
50
|
+
.bb-pill[data-tone=warn] { color: var(--bp-warning, #8b6526); border-color: color-mix(in srgb, var(--bp-warning, #8b6526) 32%, transparent); background: var(--bp-warning-bg, #fbf8f0); }
|
|
51
|
+
.bb-pill[data-tone=current] { color: var(--bp-on-solid, #fff); background: var(--bp-solid, #242427); border-color: var(--bp-solid, #242427); }
|
|
52
|
+
|
|
53
|
+
.bb-note { margin: 0; color: var(--bb-muted, #6b6b73); font-size: 12px; line-height: 1.6; }
|
|
54
|
+
.bb-note strong { color: var(--bp-ink, #242427); font-weight: 550; }
|
|
55
|
+
.bb-warn { margin: 0; padding: 10px 12px; border: 1px solid color-mix(in srgb, var(--bp-warning, #8b6526) 22%, transparent); border-radius: 9px; background: var(--bp-warning-bg, #fbf8f0); color: var(--bp-warning, #8b6526); font-size: 12px; line-height: 1.6; }
|
|
56
|
+
.bb-warn ul { margin: 6px 0 0; padding-left: 18px; }
|
|
57
|
+
.bb-danger { margin: 0; padding: 10px 12px; border: 1px solid color-mix(in srgb, var(--bp-danger, #b33c42) 24%, transparent); border-radius: 9px; background: var(--bp-danger-bg, #fcf2f2); color: var(--bp-danger, #b33c42); font-size: 12px; line-height: 1.6; }
|
|
58
|
+
.bb-danger ul { margin: 6px 0 0; padding-left: 18px; }
|
|
59
|
+
|
|
60
|
+
.bb-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px 14px; }
|
|
61
|
+
/* The editor keeps the parent form's `bp-form` class, so field and check rows are
|
|
62
|
+
scoped with `.bb-root` to win over the generic `.bp-form label` grid rule. */
|
|
63
|
+
.bb-root .bb-field { display: grid; gap: 5px; min-width: 0; font-size: 12px; color: var(--bb-muted, #6b6b73); }
|
|
64
|
+
.bb-field > span { overflow-wrap: anywhere; }
|
|
65
|
+
.bb-field :is(input, select) { width: 100%; min-width: 0; }
|
|
66
|
+
.bb-field-wide { grid-column: 1 / -1; }
|
|
67
|
+
.bb-root .bb-check { display: flex; align-items: flex-start; gap: 8px; font-size: 12px; color: var(--bp-ink, #242427); }
|
|
68
|
+
.bb-check input { margin: 3px 0 0; flex: none; }
|
|
69
|
+
.bb-check small { display: block; margin-top: 2px; color: var(--bb-muted, #6b6b73); font-size: 11px; }
|
|
70
|
+
.bb-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
|
|
71
|
+
.bb-row-end { justify-content: flex-end; }
|
|
72
|
+
.bb-link { padding: 3px 7px; min-height: 27px; border-color: transparent; background: none; color: var(--bb-muted, #6b6b73); font-size: 11px; }
|
|
73
|
+
.bb-link:hover { background: var(--bp-subtle, #f6f6f7); border-color: var(--bp-border, #e6e6e9); color: var(--bp-ink, #242427); }
|
|
74
|
+
|
|
75
|
+
.bb-fold { border: 1px solid var(--bp-border, #e6e6e9); border-radius: 11px; background: var(--bp-surface, #fff); min-width: 0; }
|
|
76
|
+
.bb-fold > summary { display: flex; align-items: center; gap: 9px; padding: 11px 14px; list-style: none; cursor: pointer; font-size: 13px; font-weight: 550; color: var(--bp-ink, #242427); }
|
|
77
|
+
.bb-fold > summary::-webkit-details-marker { display: none; }
|
|
78
|
+
.bb-fold > summary::after { content: '+'; margin-left: auto; color: var(--bb-muted, #6b6b73); font-size: 15px; font-weight: 300; }
|
|
79
|
+
.bb-fold[open] > summary::after { content: '−'; }
|
|
80
|
+
.bb-fold > summary small { color: var(--bb-muted, #6b6b73); font-size: 11px; font-weight: 400; min-width: 0; flex: 1 1 130px; overflow-wrap: anywhere; }
|
|
81
|
+
.bb-fold-body { display: flex; flex-direction: column; gap: 12px; padding: 0 14px 14px; }
|
|
82
|
+
|
|
83
|
+
/* The official tools module is deliberately its own region: it grants an extra
|
|
84
|
+
execution path, it is not the Bot's chat engine and not a model picker. */
|
|
85
|
+
.bb-tools { border: 1px solid var(--bp-border, #e6e6e9); border-radius: 12px; background: var(--bp-subtle, #f6f6f7); padding: 15px; min-width: 0; }
|
|
86
|
+
.bb-tools-head { display: flex; flex-direction: column; gap: 4px; }
|
|
87
|
+
.bb-tools-head strong { font-size: 13px; font-weight: 600; color: var(--bp-ink, #242427); }
|
|
88
|
+
.bb-tools-head p { margin: 0; color: var(--bb-muted, #6b6b73); font-size: 12px; line-height: 1.6; }
|
|
89
|
+
.bb-tool-list { display: flex; flex-direction: column; gap: 10px; margin: 12px 0 0; padding: 0; list-style: none; }
|
|
90
|
+
.bb-tool { border: 1px solid var(--bp-border, #e6e6e9); border-radius: 10px; background: var(--bp-surface, #fff); padding: 12px 13px; }
|
|
91
|
+
.bb-tool[data-granted=true] { border-color: color-mix(in srgb, var(--bp-solid, #242427) 40%, var(--bp-border, #e6e6e9)); }
|
|
92
|
+
.bb-tool[data-stale=true] { border-color: color-mix(in srgb, var(--bp-warning, #8b6526) 46%, var(--bp-border, #e6e6e9)); }
|
|
93
|
+
.bb-tool-head { display: flex; align-items: flex-start; gap: 10px; }
|
|
94
|
+
.bb-tool-head strong { font-size: 13px; font-weight: 550; color: var(--bp-ink, #242427); }
|
|
95
|
+
.bb-tool-head p { margin: 2px 0 0; color: var(--bb-muted, #6b6b73); font-size: 11px; line-height: 1.55; }
|
|
96
|
+
.bb-tool-body { display: flex; flex-direction: column; gap: 9px; margin-top: 10px; }
|
|
97
|
+
.bb-tool-title { min-width: 0; flex: 1; }
|
|
98
|
+
.bb-tool-empty { margin: 0; color: var(--bb-muted, #6b6b73); font-size: 12px; line-height: 1.6; }
|
|
99
|
+
.bb-tools-foot { margin: 12px 0 0; color: var(--bb-muted, #6b6b73); font-size: 11px; line-height: 1.6; }
|
|
100
|
+
.bb-fieldset { display: flex; flex-direction: column; gap: 10px; min-width: 0; margin: 0; padding: 0; border: 0; }
|
|
101
|
+
.bb-fieldset > legend { padding: 0; }
|
|
102
|
+
.bb-route-editor { display: grid; gap: 12px; padding: 14px; min-width: 0; border: 1px solid var(--bp-border); border-radius: 12px; }
|
|
103
|
+
.bb-route-heading { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 6px 12px; }
|
|
104
|
+
.bb-route-heading strong { display: inline-flex; align-items: center; gap: 8px; font-size: 14px; }
|
|
105
|
+
.bb-route-heading p { flex-basis: 100%; }
|
|
106
|
+
.bb-route { min-width: 0; border: 1px solid var(--bp-border); border-radius: 10px; background: var(--bp-surface); }
|
|
107
|
+
.bb-route > summary { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 11px 13px; cursor: pointer; }
|
|
108
|
+
.bb-route > summary > span:first-child { min-width: 0; display: grid; gap: 2px; overflow-wrap: anywhere; }
|
|
109
|
+
.bb-route > summary strong { font-size: 13px; }
|
|
110
|
+
.bb-route > summary small { color: var(--bb-muted, var(--bp-muted)); font-size: 11px; }
|
|
111
|
+
.bb-route > summary .bb-pill { flex: none; }
|
|
112
|
+
.bb-route[open] > summary { margin-bottom: 9px; border-bottom: 1px solid var(--bp-border); }
|
|
113
|
+
.bb-route-account { display: grid; gap: 8px; }
|
|
114
|
+
.bb-route-account > button { justify-self: start; }
|
|
115
|
+
|
|
116
|
+
.bb-save { display: flex; flex-direction: column; gap: 10px; padding-top: 4px; border-top: 1px solid var(--bp-border, #e6e6e9); }
|
|
117
|
+
.bb-save-row { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
|
|
118
|
+
.bb-save-row .bp-button-primary { margin-left: auto; }
|
|
119
|
+
.bb-feedback { margin: 0; font-size: 12px; line-height: 1.6; }
|
|
120
|
+
.bb-feedback[data-kind=saving] { color: var(--bb-muted, #6b6b73); }
|
|
121
|
+
.bb-feedback[data-kind=saved] { color: #3f7a54; }
|
|
122
|
+
.bb-feedback[data-kind=error] { color: var(--bp-danger, #b33c42); }
|
|
123
|
+
.bb-feedback[data-kind=notice] { color: var(--bp-warning, #8b6526); }
|
|
124
|
+
|
|
125
|
+
@container (max-width: 560px) {
|
|
126
|
+
.bb-fields { grid-template-columns: 1fr; }
|
|
127
|
+
.bb-step-head { flex-wrap: wrap; }
|
|
128
|
+
.bb-step-state { margin-left: 33px; }
|
|
129
|
+
.bb-summary { gap: 4px 12px; }
|
|
130
|
+
.bb-summary div { flex-basis: 100%; }
|
|
131
|
+
.bb-list-scroll { max-height: none; }
|
|
132
|
+
.bb-save-row .bp-button-primary { margin-left: 0; width: 100%; }
|
|
133
|
+
}
|
|
134
|
+
html body[data-bot-product-ui] .bb-root .bb-model-card { padding: 18px; border: 1px solid var(--bb-line, var(--bp-border)); border-radius: 12px; background: var(--bb-surface, var(--bp-surface)); }
|
|
135
|
+
html body[data-bot-product-ui] .bb-root .bb-budget-config { border: 1px solid var(--bb-line, var(--bp-border)); border-radius: 10px; }
|
|
136
|
+
html body[data-bot-product-ui] .bb-root .bb-budget-config > summary { display: flex; align-items: center; gap: 9px; padding: 13px 15px; cursor: pointer; color: var(--bb-ink, var(--bp-ink)); font-size: 13px; }
|
|
137
|
+
html body[data-bot-product-ui] .bb-root .bb-budget-config > summary .bb-pill { margin-left: auto; }
|
|
138
|
+
html body[data-bot-product-ui] .bb-root .bb-budget-config .bb-step { border: 0; }
|
|
139
|
+
html body[data-bot-product-ui] .bb-root .bb-tools-head { display: flex; flex-direction: row; flex-wrap: wrap; align-items: center; gap: 8px; }
|
|
140
|
+
html body[data-bot-product-ui] .bb-root .bb-tools-head > p { flex-basis: 100%; margin-top: 0; }
|
|
141
|
+
html body[data-bot-product-ui] .bb-root .bb-tools-head > button { margin-left: auto; }
|
|
142
|
+
html body[data-bot-product-ui] .bb-root .bb-overview > summary { display: flex; align-items: center; justify-content: space-between; padding: 8px 0; cursor: pointer; font-size: 12px; color: var(--bb-muted); }
|
|
143
|
+
html body[data-bot-product-ui] .bb-root .bb-overview > summary span { font-size: 11px; }
|
|
144
|
+
html body[data-bot-product-ui] .bb-root .bb-model-applied { margin: 10px 0 0; color: var(--bb-muted); font-size: 12px; line-height: 1.5; overflow-wrap: anywhere; }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const ref = value => typeof value === 'string' && value.length > 0 && value.length <= 512 && !/[\r\n\0]/.test(value) ? value : null
|
|
2
|
+
const entryRef = value => value?.ref ?? (typeof value?.provider === 'string' && typeof value?.model === 'string' ? `${value.provider}/${value.model}` : null)
|
|
3
|
+
|
|
4
|
+
/** Applied is the Host's last saved selection, not proof of this turn's actual response model. */
|
|
5
|
+
export function botModelStatus(bot, catalog = []) {
|
|
6
|
+
const selected = ref(bot?.model), applied = ref(bot?.appliedModel)
|
|
7
|
+
const label = value => {
|
|
8
|
+
const entry = Array.isArray(catalog) ? catalog.find(item => entryRef(item) === value) : undefined
|
|
9
|
+
return typeof entry?.name === 'string' && entry.name ? entry.name : typeof entry?.label === 'string' && entry.label ? entry.label : value
|
|
10
|
+
}
|
|
11
|
+
const selectedLabel = selected ? label(selected) : null, appliedLabel = applied ? label(applied) : null
|
|
12
|
+
const different = Boolean(selected && applied && selected !== applied)
|
|
13
|
+
const title = [selected ? `宿主已选:${selected}` : '宿主未指定单独模型',
|
|
14
|
+
applied ? `最近应用:${applied}` : '宿主尚未报告应用记录'].join(';')
|
|
15
|
+
return { selected, applied, selectedLabel, appliedLabel, different, title,
|
|
16
|
+
label: selectedLabel ?? appliedLabel ?? '选择模型',
|
|
17
|
+
appliedText: applied ? `最近应用:${appliedLabel}${different ? ' · 与宿主已选模型不同' : ''}` : '宿主尚未报告应用记录' }
|
|
18
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import { UiIcon } from './ui-icons.jsx'
|
|
3
|
+
import css from './bot-profile.css'
|
|
4
|
+
import {
|
|
5
|
+
BOT_PROFILE_FIELDS, BOT_PROFILE_FIELD_META, BOT_PROFILE_GROUPS, botProfileDirty, botProfileDraft, botProfileFailure,
|
|
6
|
+
botProfilePatch, botProfileProblems, botProfileSavedText, botProfileSearch,
|
|
7
|
+
} from './bot-profile.mjs'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Identity and personality, inside the Bot settings side window.
|
|
11
|
+
*
|
|
12
|
+
* It writes only what the Host persists — name, role, instructions — through the
|
|
13
|
+
* owner product actions `botProfile.inspect` / `botProfile.save`, so the same
|
|
14
|
+
* window that picks the model and the official tools also decides who this Bot
|
|
15
|
+
* is. Three rules shape the panel:
|
|
16
|
+
*
|
|
17
|
+
* - the Host's committed record is the baseline, never the draft: a save sends
|
|
18
|
+
* the changed fields plus the values they were read at, so an edit made in
|
|
19
|
+
* another window is refused instead of overwritten, and an unrelated save
|
|
20
|
+
* never blocks this one;
|
|
21
|
+
* - a response that arrives late — a slow save, a switched Bot, a re-read —
|
|
22
|
+
* updates the baseline, never the draft, so a keystroke made while a save was
|
|
23
|
+
* in flight is never lost;
|
|
24
|
+
* - composing text is never a submit: an IMEEnter and a form submit during
|
|
25
|
+
* composition are both dropped, and nothing resets the draft underneath the
|
|
26
|
+
* composition.
|
|
27
|
+
*
|
|
28
|
+
* Instructions are text, not a grant: the panel says so where the field is, and
|
|
29
|
+
* it never offers a model, a tool, a permission or a working directory.
|
|
30
|
+
*/
|
|
31
|
+
export function BotProfileIdentity({ productApi, botId, busy = false, onSaved }) {
|
|
32
|
+
const [view, setView] = useState(null), [draft, setDraft] = useState(null), [query, setQuery] = useState('')
|
|
33
|
+
const [open, setOpen] = useState(() => new Set(BOT_PROFILE_GROUPS.map(group => group.id)))
|
|
34
|
+
const [loading, setLoading] = useState(true), [saving, setSaving] = useState(false), [error, setError] = useState('')
|
|
35
|
+
const [feedback, setFeedback] = useState(null)
|
|
36
|
+
const session = useRef({ live: false, epoch: 0, saving: false, requests: new Set() })
|
|
37
|
+
const saveToken = useRef(0)
|
|
38
|
+
const composing = useRef(false)
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
// The stylesheet ships with this panel so the parent needs one import and one
|
|
42
|
+
// render; a parent that concatenates it into the dialog's own style must not
|
|
43
|
+
// do both at once.
|
|
44
|
+
const style = document.createElement('style')
|
|
45
|
+
style.dataset.botProfileIdentity = '1'
|
|
46
|
+
style.textContent = css
|
|
47
|
+
document.head.append(style)
|
|
48
|
+
return () => { style.remove() }
|
|
49
|
+
}, [])
|
|
50
|
+
|
|
51
|
+
const call = async (action, payload, signal) => {
|
|
52
|
+
const controller = new AbortController()
|
|
53
|
+
const abort = () => controller.abort()
|
|
54
|
+
if (signal?.aborted) abort()
|
|
55
|
+
else signal?.addEventListener('abort', abort, { once: true })
|
|
56
|
+
session.current.requests.add(controller)
|
|
57
|
+
let rejectAborted
|
|
58
|
+
const deadline = new Promise((_resolve, reject) => { rejectAborted = () => reject(Object.assign(new Error('REQUEST_ABORTED'), { code: 'REQUEST_ABORTED' })); controller.signal.addEventListener('abort', rejectAborted, { once: true }) })
|
|
59
|
+
const timer = setTimeout(abort, 10000)
|
|
60
|
+
if (controller.signal.aborted) rejectAborted()
|
|
61
|
+
try { return await Promise.race([Promise.resolve().then(() => productApi.dispatch(action, payload, controller.signal)), deadline]) }
|
|
62
|
+
finally { clearTimeout(timer); signal?.removeEventListener('abort', abort); controller.signal.removeEventListener('abort', rejectAborted); session.current.requests.delete(controller) }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const read = async () => {
|
|
66
|
+
const current = session.current, epoch = ++current.epoch
|
|
67
|
+
setLoading(true)
|
|
68
|
+
try {
|
|
69
|
+
const value = await call('botProfile.inspect', { botId })
|
|
70
|
+
if (!current.live || epoch !== current.epoch) return
|
|
71
|
+
if (value?.schemaVersion !== 1 || value.bot?.id !== botId || !Number.isSafeInteger(value.revision) || value.revision < 0
|
|
72
|
+
|| !BOT_PROFILE_FIELDS.every(field => typeof value.bot[field] === 'string')) throw Object.assign(new Error('WRONG_PROFILE_OWNER'), { code: 'WRONG_PROFILE_OWNER' })
|
|
73
|
+
setView(value); setDraft(botProfileDraft(value)); setError('')
|
|
74
|
+
setFeedback(currentFeedback => (currentFeedback?.kind === 'saved' ? currentFeedback : null))
|
|
75
|
+
} catch (cause) {
|
|
76
|
+
if (!current.live || epoch !== current.epoch) return
|
|
77
|
+
if (cause?.code === 'REQUEST_ABORTED') { setError('读取结果尚未确认,请重新读取后核对。'); return }
|
|
78
|
+
setError(['PRODUCT_ACTION_UNAVAILABLE', 'INVALID_PRODUCT_ACTION'].includes(cause?.code) ? '当前连接尚不支持身份与个性设置,请更新完整插件。' : '暂时无法读取这个 Bot 的身份,请重试。')
|
|
79
|
+
} finally { if (current.live && epoch === current.epoch) setLoading(false) }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
const current = session.current
|
|
84
|
+
current.live = true
|
|
85
|
+
// A Bot switch invalidates the previous Bot's read and its in-flight save,
|
|
86
|
+
// and drops its values from the panel: no other Bot's identity is ever
|
|
87
|
+
// shown as this one's.
|
|
88
|
+
current.epoch += 1; saveToken.current += 1; current.saving = false; setSaving(false)
|
|
89
|
+
for (const controller of current.requests) controller.abort()
|
|
90
|
+
setView(null); setDraft(null); setFeedback(null)
|
|
91
|
+
void read()
|
|
92
|
+
return () => {
|
|
93
|
+
current.live = false; current.epoch += 1; saveToken.current += 1
|
|
94
|
+
for (const controller of current.requests) controller.abort()
|
|
95
|
+
}
|
|
96
|
+
}, [productApi, botId])
|
|
97
|
+
|
|
98
|
+
const edit = (field, value) => { setDraft(current => (current ? { ...current, [field]: value } : current)); setFeedback(null) }
|
|
99
|
+
const reload = () => { void read() }
|
|
100
|
+
|
|
101
|
+
const dirty = view && draft ? botProfileDirty(draft, view) : false
|
|
102
|
+
const problems = useMemo(() => (draft ? botProfileProblems(draft) : []), [draft])
|
|
103
|
+
const search = useMemo(() => botProfileSearch(query), [query])
|
|
104
|
+
const groupOpen = id => (search ? search.groups[id] : open.has(id))
|
|
105
|
+
const toggle = id => setOpen(current => { const next = new Set(current); next.has(id) ? next.delete(id) : next.add(id); return next })
|
|
106
|
+
// The committed record belongs to one Bot: until this Bot's own read lands,
|
|
107
|
+
// nothing of another Bot's identity is shown or offered for saving, not even
|
|
108
|
+
// for the frame between the switch and its read.
|
|
109
|
+
const owned = view && draft && view.bot.id === botId ? view : null
|
|
110
|
+
|
|
111
|
+
const save = async event => {
|
|
112
|
+
if (event) event.preventDefault()
|
|
113
|
+
const current = session.current
|
|
114
|
+
// An IME composition is never a submit, and a second click in the same batch
|
|
115
|
+
// must not send the same revision twice.
|
|
116
|
+
if (composing.current || !current.live || current.saving || busy || !owned || !dirty || problems.length > 0) return
|
|
117
|
+
const profile = botProfilePatch(draft, view)
|
|
118
|
+
if (Object.keys(profile).length === 0) return
|
|
119
|
+
const token = saveToken.current
|
|
120
|
+
const sentDraft = { ...draft }, sentView = view
|
|
121
|
+
current.saving = true; setSaving(true)
|
|
122
|
+
setFeedback({ kind: 'saving', text: '正在保存到宿主…' })
|
|
123
|
+
try {
|
|
124
|
+
const result = await call('botProfile.save', { botId, expectedRevision: view.revision ?? 0, base: botProfileDraft(view), profile })
|
|
125
|
+
if (!current.live || token !== saveToken.current) return
|
|
126
|
+
const expected = Object.keys(profile).filter(field => profile[field].trim() !== (sentView.bot[field] ?? '')).sort()
|
|
127
|
+
const valid = result?.schemaVersion === 1 && result.botId === botId && result.bot?.id === botId
|
|
128
|
+
&& Number.isSafeInteger(result.revision) && result.revision >= sentView.revision
|
|
129
|
+
&& BOT_PROFILE_FIELDS.every(field => typeof result.bot[field] === 'string')
|
|
130
|
+
&& Array.isArray(result.saved) && result.saved.length === expected.length
|
|
131
|
+
&& [...result.saved].sort().every((field, index) => field === expected[index])
|
|
132
|
+
&& Object.keys(profile).every(field => result.bot[field] === profile[field].trim())
|
|
133
|
+
if (!valid) throw Object.assign(new Error('宿主尚未确认这次身份保存,请重新读取后核对。'), { code: 'BOT_PROFILE_UNCONFIRMED' })
|
|
134
|
+
// The acknowledged record becomes the new baseline. Anything typed while
|
|
135
|
+
// the save was in flight stays in the draft and simply reads as unsaved.
|
|
136
|
+
setView(currentView => (currentView ? { ...currentView, bot: result.bot, revision: result.revision } : currentView))
|
|
137
|
+
setDraft(currentDraft => currentDraft ? Object.fromEntries(BOT_PROFILE_FIELDS.map(field => [field,
|
|
138
|
+
currentDraft[field] === sentDraft[field] ? result.bot[field] : currentDraft[field]])) : currentDraft)
|
|
139
|
+
setFeedback({ kind: 'saved', text: botProfileSavedText(result) })
|
|
140
|
+
onSaved?.(result.bot)
|
|
141
|
+
} catch (cause) {
|
|
142
|
+
if (!current.live || token !== saveToken.current) return
|
|
143
|
+
setFeedback({ kind: cause?.code === 'BOT_PROFILE_CONFLICT' ? 'notice' : 'error', text: botProfileFailure(cause) })
|
|
144
|
+
} finally {
|
|
145
|
+
if (current.live && token === saveToken.current) { current.saving = false; setSaving(false) }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const scope = `${botId}`
|
|
150
|
+
return <section className="bp-profile" aria-label="身份与个性">
|
|
151
|
+
<header className="bp-profile-head">
|
|
152
|
+
<span className="bp-profile-badge" aria-hidden="true"><UiIcon name="bot" size={18}/></span>
|
|
153
|
+
<div className="bp-profile-title"><h3>身份与个性</h3><p>名称、职责与对话指令;保存只写回这个 Bot 的资料,不会代替你给它发消息。</p></div>
|
|
154
|
+
{owned && <span className="bp-profile-revision" title="宿主已提交状态的修订号">修订号 {owned.revision ?? '—'}</span>}
|
|
155
|
+
</header>
|
|
156
|
+
<div className="bp-profile-search">
|
|
157
|
+
<UiIcon name="search" size={15}/>
|
|
158
|
+
<input type="search" value={query} aria-label="搜索身份设置" placeholder="搜索名称、职责、指令…" onChange={event => setQuery(event.target.value)}/>
|
|
159
|
+
{query !== '' && <button type="button" aria-label="清除搜索" onClick={() => setQuery('')}><UiIcon name="close" size={13}/></button>}
|
|
160
|
+
</div>
|
|
161
|
+
{loading && !view && <p className="bp-muted bp-profile-status" role="status">正在读取 Bot 身份…</p>}
|
|
162
|
+
{error !== '' && <div className="bp-profile-error" role="alert"><p>{error}</p><button type="button" disabled={busy || loading} onClick={reload}>重新读取</button></div>}
|
|
163
|
+
{owned && <>
|
|
164
|
+
{search && !search.any && <p className="bp-muted bp-profile-status" role="status">没有匹配“{query}”的身份设置。</p>}
|
|
165
|
+
{BOT_PROFILE_GROUPS.map(group => <section key={group.id} className="bp-profile-group" data-group={group.id} data-open={groupOpen(group.id) ? 'true' : 'false'} aria-label={group.label}>
|
|
166
|
+
<button type="button" className="bp-profile-group-head" aria-expanded={groupOpen(group.id)} onClick={() => (search ? setQuery('') : toggle(group.id))}>
|
|
167
|
+
<UiIcon name={group.icon} size={16}/>
|
|
168
|
+
<span className="bp-profile-group-label">{group.label}</span>
|
|
169
|
+
<span className="bp-profile-group-hint">{group.hint}</span>
|
|
170
|
+
<UiIcon name="chevron" size={15}/>
|
|
171
|
+
</button>
|
|
172
|
+
{groupOpen(group.id) && <div className="bp-profile-group-body">
|
|
173
|
+
{group.fields.map(field => { const meta = BOT_PROFILE_FIELD_META[field], problem = problems.find(item => item.field === field)
|
|
174
|
+
return <label key={field} className="bp-profile-field" htmlFor={`bp-profile-${scope}-${field}`}>
|
|
175
|
+
<span className="bp-profile-field-head"><span className="bp-profile-field-label">{meta.label}</span>
|
|
176
|
+
<span className="bp-profile-field-count">{draft[field].length}/{meta.limit}</span></span>
|
|
177
|
+
{meta.control === 'input'
|
|
178
|
+
? <input id={`bp-profile-${scope}-${field}`} type="text" value={draft[field]} maxLength={meta.limit} placeholder={meta.placeholder} disabled={busy || saving}
|
|
179
|
+
onChange={event => edit(field, event.target.value)}
|
|
180
|
+
onCompositionStart={() => { composing.current = true }} onCompositionEnd={() => { composing.current = false }}/>
|
|
181
|
+
: <textarea id={`bp-profile-${scope}-${field}`} value={draft[field]} maxLength={meta.limit} rows={5} placeholder={meta.placeholder} disabled={busy || saving}
|
|
182
|
+
onChange={event => edit(field, event.target.value)}
|
|
183
|
+
onCompositionStart={() => { composing.current = true }} onCompositionEnd={() => { composing.current = false }}/>}
|
|
184
|
+
<small className="bp-profile-field-help">{meta.help}</small>
|
|
185
|
+
{problem && <small className="bp-profile-field-problem" role="status">{problem.text}</small>}
|
|
186
|
+
</label> })}
|
|
187
|
+
</div>}
|
|
188
|
+
</section>)}
|
|
189
|
+
<form className="bp-profile-save" onSubmit={save} noValidate>
|
|
190
|
+
<span className="bp-profile-pill" data-tone={dirty ? 'warn' : 'muted'}>{dirty ? '有未保存修改' : '与宿主一致'}</span>
|
|
191
|
+
<button type="submit" className="bp-button-primary" disabled={busy || saving || !dirty || problems.length > 0}>
|
|
192
|
+
{saving ? '保存中…' : '保存身份与指令'}</button>
|
|
193
|
+
</form>
|
|
194
|
+
</>}
|
|
195
|
+
{feedback && <p className="bp-profile-feedback" data-kind={feedback.kind} role="status">{feedback.text}</p>}
|
|
196
|
+
</section>
|
|
197
|
+
}
|