@xmanrui/dsh-im 0.3.0 → 0.5.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/README.md +64 -0
- package/assets/logo-icon.png +0 -0
- package/assets/logo.png +0 -0
- package/lib/client.js +765 -295
- package/lib/index.js +112 -110
- package/package.json +2 -1
- package/plugin-src/client/build.mjs +6 -1
- package/plugin-src/client/channels/dingtalk/api.js +2 -0
- package/plugin-src/client/channels/dingtalk/index.js +67 -14
- package/plugin-src/client/channels/feishu/api.js +2 -0
- package/plugin-src/client/channels/feishu/index.js +70 -22
- package/plugin-src/client/channels/qq/api.js +2 -0
- package/plugin-src/client/channels/qq/index.js +45 -8
- package/plugin-src/client/channels/shared/token-api.js +2 -0
- package/plugin-src/client/channels/shared/token-channel.js +34 -8
- package/plugin-src/client/channels/wecom/api.js +2 -0
- package/plugin-src/client/channels/wecom/index.js +45 -8
- package/plugin-src/client/channels/weixin/api.js +2 -0
- package/plugin-src/client/channels/weixin/index.js +68 -8
- package/plugin-src/client/channels/whatsapp/api.js +2 -0
- package/plugin-src/client/channels/whatsapp/index.js +27 -5
- package/plugin-src/client/i18n.js +15 -1
- package/plugin-src/client/index.js +28 -1
- package/plugin-src/client/styles.js +32 -9
- package/plugin-src/client/workspace-editor.js +96 -0
- package/plugin-src/client/workspace-snapshot-fence.js +28 -0
- package/plugin-src/host/channels/dingtalk/production.mjs +34 -11
- package/plugin-src/host/channels/dingtalk/rpc.mjs +17 -2
- package/plugin-src/host/channels/feishu/production.mjs +42 -5
- package/plugin-src/host/channels/feishu/rpc.mjs +17 -2
- package/plugin-src/host/channels/qq/production.mjs +48 -22
- package/plugin-src/host/channels/qq/rpc.mjs +16 -2
- package/plugin-src/host/channels/shared/production.mjs +48 -22
- package/plugin-src/host/channels/shared/rpc.mjs +15 -0
- package/plugin-src/host/channels/shared/workspace-rpc.mjs +25 -0
- package/plugin-src/host/channels/slack/production.mjs +48 -23
- package/plugin-src/host/channels/slack/rpc.mjs +16 -0
- package/plugin-src/host/channels/wecom/production.mjs +49 -23
- package/plugin-src/host/channels/wecom/rpc.mjs +16 -2
- package/plugin-src/host/channels/weixin/production.mjs +31 -11
- package/plugin-src/host/channels/weixin/rpc.mjs +21 -2
- package/plugin-src/host/channels/whatsapp/production.mjs +49 -23
- package/plugin-src/host/channels/whatsapp/rpc.mjs +16 -2
- package/src/channels/dingtalk/dingtalk-bridge.mjs +22 -11
- package/src/channels/dingtalk/dingtalk-controller.mjs +6 -1
- package/src/channels/dingtalk/harness-client.mjs +4 -3
- package/src/channels/dingtalk/state-store.mjs +5 -0
- package/src/channels/discord/discord-api.mjs +1 -1
- package/src/channels/feishu/bridge.mjs +36 -16
- package/src/channels/feishu/harness-client.mjs +6 -5
- package/src/channels/feishu/state-store.mjs +5 -0
- package/src/channels/qq/qq-bridge.mjs +25 -15
- package/src/channels/qq/qq-controller.mjs +8 -1
- package/src/channels/qq/state-store.mjs +5 -0
- package/src/channels/shared/bot-workspace-store.mjs +584 -0
- package/src/channels/shared/conversation-state-store.mjs +5 -0
- package/src/channels/shared/text-harness-bridge.mjs +23 -13
- package/src/channels/shared/workspace-command.mjs +26 -0
- package/src/channels/shared/workspace-session.mjs +44 -0
- package/src/channels/wecom/state-store.mjs +5 -0
- package/src/channels/wecom/wecom-bridge.mjs +22 -13
- package/src/channels/wecom/wecom-controller.mjs +8 -1
- package/src/channels/weixin/harness-client.mjs +6 -5
- package/src/channels/weixin/state-store.mjs +5 -0
- package/src/channels/weixin/weixin-api.mjs +1 -1
- package/src/channels/weixin/weixin-bridge.mjs +16 -6
- package/src/channels/weixin/weixin-controller.mjs +6 -1
- package/src/channels/whatsapp/whatsapp-controller.mjs +8 -1
|
@@ -43,6 +43,8 @@ import { installImStyles } from './styles.js';
|
|
|
43
43
|
export const name = 'im-settings';
|
|
44
44
|
export const inject = ['slots', 'connection', 'locale'];
|
|
45
45
|
|
|
46
|
+
const IM_PLUGIN_LOGO_URL = globalThis.__DSH_IM_LOGO_DATA_URL__ ?? 'assets/logo-icon.png';
|
|
47
|
+
|
|
46
48
|
const CHANNELS = Object.freeze([
|
|
47
49
|
{ id: 'weixin', label: '微信' },
|
|
48
50
|
{ id: 'feishu', label: '飞书' },
|
|
@@ -122,10 +124,35 @@ export function IMSettingsTab({
|
|
|
122
124
|
whatsappRpcCall,
|
|
123
125
|
}) {
|
|
124
126
|
const [selected, setSelected] = React.useState('weixin');
|
|
127
|
+
const githubTooltipId = React.useId();
|
|
125
128
|
const active = CHANNELS.find((channel) => channel.id === selected) ?? CHANNELS[0];
|
|
126
129
|
return h('section', { className: 'dim-page', 'aria-label': 'IM机器人设置' },
|
|
127
130
|
h('header', { className: 'dim-title' },
|
|
128
|
-
h('
|
|
131
|
+
h('div', { className: 'dim-brand' },
|
|
132
|
+
h('img', {
|
|
133
|
+
className: 'dim-brandLogo',
|
|
134
|
+
src: IM_PLUGIN_LOGO_URL,
|
|
135
|
+
alt: 'dsh-im',
|
|
136
|
+
width: 48,
|
|
137
|
+
height: 48,
|
|
138
|
+
}),
|
|
139
|
+
h('p', null, '让聊天机器人轻松接入 DeepSeek Harness')),
|
|
140
|
+
h('span', { className: 'dim-githubAction' },
|
|
141
|
+
h('a', {
|
|
142
|
+
className: 'dim-githubLink',
|
|
143
|
+
href: 'https://github.com/xmanrui/dsh-im',
|
|
144
|
+
target: '_blank',
|
|
145
|
+
rel: 'noopener noreferrer',
|
|
146
|
+
'aria-label': 'dsh-im GitHub',
|
|
147
|
+
'aria-describedby': githubTooltipId,
|
|
148
|
+
},
|
|
149
|
+
h('span', null, 'GitHub'),
|
|
150
|
+
h('span', { className: 'dim-githubArrow', 'aria-hidden': 'true' }, '↗')),
|
|
151
|
+
h('span', {
|
|
152
|
+
id: githubTooltipId,
|
|
153
|
+
className: 'dim-githubTooltip',
|
|
154
|
+
role: 'tooltip',
|
|
155
|
+
}, '帮助与反馈 · 前往 GitHub')),
|
|
129
156
|
),
|
|
130
157
|
h('div', { className: 'dim-layout' },
|
|
131
158
|
h('nav', { className: 'dim-rail', role: 'tablist', 'aria-label': 'IM 渠道' },
|
|
@@ -11,8 +11,17 @@ const CSS = String.raw`
|
|
|
11
11
|
box-sizing: border-box;
|
|
12
12
|
}
|
|
13
13
|
.dim-page *, .dim-page *::before, .dim-page *::after { box-sizing: border-box; }
|
|
14
|
-
.dim-title { margin: 0 0
|
|
15
|
-
.dim-
|
|
14
|
+
.dim-title { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin: 0 0 18px; }
|
|
15
|
+
.dim-brand { min-width: 0; display: flex; align-items: center; gap: 12px; }
|
|
16
|
+
.dim-brandLogo { width: 48px; height: 48px; flex: 0 0 48px; display: block; object-fit: contain; filter: grayscale(1) contrast(1.08); }
|
|
17
|
+
.dim-title p { margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 14px; line-height: 20px; font-weight: 600; white-space: nowrap; }
|
|
18
|
+
.dim-githubAction { position: relative; display: inline-flex; flex: none; }
|
|
19
|
+
.dim-githubLink { min-height: 30px; display: inline-flex; align-items: center; gap: 5px; flex: none; padding: 0 10px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-bg-layer-1, #fff); font-size: 12px; line-height: normal; font-weight: 560; text-decoration: none; transition: border-color .15s ease, color .15s ease, background .15s ease; }
|
|
20
|
+
.dim-githubLink:hover { border-color: #aeb3bb; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
|
|
21
|
+
.dim-githubLink:focus-visible { outline: 2px solid color-mix(in srgb, var(--dim-blue) 70%, white); outline-offset: 2px; }
|
|
22
|
+
.dim-githubArrow { font-size: 13px; line-height: 1; }
|
|
23
|
+
.dim-githubTooltip { position: absolute; right: 0; bottom: calc(100% + 8px); z-index: 20; width: max-content; max-width: min(220px, 80vw); padding: 6px 9px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 7px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-3, #fff); box-shadow: 0 8px 24px rgb(31 35 41 / 14%); font-size: 11px; line-height: 16px; font-weight: 500; white-space: nowrap; opacity: 0; visibility: hidden; transform: translateY(3px); pointer-events: none; transition: opacity .15s ease, transform .15s ease, visibility .15s ease; }
|
|
24
|
+
.dim-githubAction:hover .dim-githubTooltip, .dim-githubAction:focus-within .dim-githubTooltip { opacity: 1; visibility: visible; transform: translateY(0); }
|
|
16
25
|
.dim-layout { display: grid; grid-template-columns: 174px 1px minmax(0, 1fr); gap: 24px; align-items: start; }
|
|
17
26
|
.dim-rail { max-height: 520px; display: grid; align-content: start; gap: 8px; overflow-y: auto; padding: 1px 4px 1px 1px; scrollbar-width: thin; scrollbar-color: var(--dsw-alias-border-l2, #dfe1e5) transparent; }
|
|
18
27
|
.dim-rail::-webkit-scrollbar { width: 4px; }
|
|
@@ -57,7 +66,7 @@ const CSS = String.raw`
|
|
|
57
66
|
.dim-panel .dim-credentialButton:hover:not(:disabled) { border-color: #4e5969; background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
|
|
58
67
|
.dim-panel .dim-credentialButton[aria-pressed="true"] { border-color: #4e5969; background: var(--dsw-alias-bg-module-platform, #f2f3f5); box-shadow: inset 0 0 0 1px rgb(78 89 105 / 8%); }
|
|
59
68
|
.dim-panel .bxf-headingTools .dim-onlineBadge, .dim-panel .dxw-tools .dim-onlineBadge, .dim-panel .ddt-tools .dim-onlineBadge { min-height: 30px; display: inline-flex; align-items: center; justify-self: end; gap: 0; padding: 0 11px; border: 0; border-radius: 999px; color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-bg-module-platform, #f2f3f5); font: inherit; font-size: 12px; font-weight: 400; line-height: normal; white-space: nowrap; }
|
|
60
|
-
.dim-panel .dim-channelPage { width: 100%; max-width: none; display: flex; flex-direction: column; gap:
|
|
69
|
+
.dim-panel .dim-channelPage { width: 100%; max-width: none; display: flex; flex-direction: column; gap: 12px; padding: 0 0 24px; color: var(--dsw-alias-label-primary, #1f2329); box-sizing: border-box; }
|
|
61
70
|
.dim-panel .dim-surfaceCard { position: relative; overflow: hidden; border: 1px solid var(--dsw-alias-border-l2, #e5e6eb); border-radius: 14px; background: var(--dsw-alias-bg-layer-1, #fff); box-shadow: 0 1px 2px rgb(31 35 41 / 3%); }
|
|
62
71
|
.dim-panel .dim-surfaceCard::before { display: none; }
|
|
63
72
|
.dim-panel .dim-surfaceBody { padding: 24px; }
|
|
@@ -73,9 +82,9 @@ const CSS = String.raw`
|
|
|
73
82
|
.dim-panel .dim-credentialError { margin: 0; color: var(--dsw-alias-state-error-primary, #d54941); font-size: 12px; line-height: 1.5; }
|
|
74
83
|
.dim-panel .dim-credentialActions { margin-top: 0; }
|
|
75
84
|
.dim-panel .dim-listSection { display: flex; flex-direction: column; gap: 0; }
|
|
76
|
-
.dim-panel .dim-listHeading { min-height: 0; display: flex; align-items: center; justify-content: space-between; gap: 16px; margin:
|
|
85
|
+
.dim-panel .dim-listHeading { min-height: 0; display: flex; align-items: center; justify-content: space-between; gap: 16px; margin: 0 0 6px; padding: 0; }
|
|
77
86
|
.dim-panel .dim-listHeading h3 { margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 14px; line-height: normal; font-weight: 650; }
|
|
78
|
-
.dim-panel .dim-botList { display: grid; gap:
|
|
87
|
+
.dim-panel .dim-botList { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; }
|
|
79
88
|
.dim-panel .dim-loadingView { padding: 38px; color: var(--dsw-alias-label-secondary, #646a73); text-align: center; }
|
|
80
89
|
.dim-panel .dim-loadingView h3 { margin: 0 0 7px; color: var(--dsw-alias-label-primary, #1f2329); font-size: 17px; line-height: normal; font-weight: 650; }
|
|
81
90
|
.dim-panel .dim-loadingView p { margin: 0; line-height: 1.6; }
|
|
@@ -123,7 +132,20 @@ const CSS = String.raw`
|
|
|
123
132
|
.dim-panel .dim-confirm { padding: 18px 24px; border-top: 1px solid var(--dsw-alias-border-l1, #eef0f3); background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
|
|
124
133
|
.dim-panel .dim-confirm strong, .dim-panel .dim-confirm h4 { margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 14px; line-height: 1.4; font-weight: 650; }
|
|
125
134
|
.dim-panel .dim-confirm p { margin: 7px 0 0; color: var(--dsw-alias-label-secondary, #646a73); line-height: 1.6; }
|
|
126
|
-
.dim-panel .dim-cardFooter { display: flex; align-items: center; justify-content: space-between; gap: 15px; padding-top:
|
|
135
|
+
.dim-panel .dim-cardFooter { display: flex; align-items: center; justify-content: space-between; gap: 15px; padding-top: 8px; border-top: 1px solid var(--dsw-alias-border-l1, #eef0f3); }
|
|
136
|
+
.dim-panel .dim-workspace { min-width: 0; display: grid; gap: 7px; margin-top: 14px; padding: 11px 12px; border: 1px solid var(--dsw-alias-border-l1, #eef0f3); border-radius: 9px; background: var(--dsw-alias-bg-module-platform, #f7f8fa); }
|
|
137
|
+
.dim-panel .dim-workspaceHeader { display: flex; align-items: center; justify-content: space-between; gap: 12px; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: normal; }
|
|
138
|
+
.dim-panel .dim-workspaceEdit { padding: 0; border: 0; color: #1677ff; background: transparent; font: inherit; font-weight: 560; cursor: pointer; }
|
|
139
|
+
.dim-panel .dim-workspaceEdit:disabled { cursor: not-allowed; opacity: .55; }
|
|
140
|
+
.dim-panel .dim-workspacePath { min-width: 0; display: block; overflow: hidden; color: var(--dsw-alias-label-primary, #1f2329); font: 12px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace; text-overflow: ellipsis; white-space: nowrap; }
|
|
141
|
+
.dim-panel .dim-workspaceForm { min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr) max-content; gap: 8px; }
|
|
142
|
+
.dim-panel .dim-workspaceForm input { width: 100%; min-width: 0; height: 34px; padding: 0 10px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 7px; outline: none; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-1, #fff); font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
143
|
+
.dim-panel .dim-workspaceForm input:focus { border-color: #1677ff; box-shadow: 0 0 0 2px rgb(22 119 255 / 10%); }
|
|
144
|
+
.dim-panel .dim-workspaceActions { display: flex; gap: 6px; }
|
|
145
|
+
.dim-panel .dim-workspaceActions button { min-height: 34px; padding: 0 10px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 7px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-size: 12px; cursor: pointer; }
|
|
146
|
+
.dim-panel .dim-workspaceActions button[type="submit"] { border-color: #1677ff; color: white; background: #1677ff; }
|
|
147
|
+
.dim-panel .dim-workspaceActions button:disabled { cursor: not-allowed; opacity: .55; }
|
|
148
|
+
.dim-panel .dim-workspaceError { grid-column: 1 / -1; margin: 0; color: var(--dsw-alias-state-error-primary, #d54941); font-size: 12px; line-height: 1.5; }
|
|
127
149
|
.dim-panel .dim-cardSummary { min-width: 0; color: var(--dsw-alias-label-secondary, #646a73); font: inherit; font-size: 12px; font-weight: 400; line-height: normal; }
|
|
128
150
|
.dim-panel .dim-cardActions { flex: none; display: flex; align-items: center; flex-wrap: nowrap; gap: 8px; margin: 0 0 0 auto; }
|
|
129
151
|
.dim-panel .dim-cardActions .dim-cardAction { flex: none; min-height: 34px; padding: 0 13px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-size: 13px; font-weight: 560; line-height: normal; white-space: nowrap; }
|
|
@@ -131,7 +153,7 @@ const CSS = String.raw`
|
|
|
131
153
|
.dim-panel .dim-cardActions .dim-cardAction[data-kind="danger"] { color: var(--dsw-alias-state-error-primary, #d54941); }
|
|
132
154
|
.dim-panel .dim-botCard { position: relative; overflow: hidden; border: 1px solid var(--dsw-alias-border-l2, #e5e6eb); border-radius: 14px; background: var(--dsw-alias-bg-layer-1, #fff); box-shadow: 0 1px 2px rgb(31 35 41 / 3%); }
|
|
133
155
|
.dim-panel .dim-botCard::before { display: none; }
|
|
134
|
-
.dim-panel .dim-botCardBody { position: relative; padding:
|
|
156
|
+
.dim-panel .dim-botCardBody { position: relative; padding: 14px; }
|
|
135
157
|
.dim-panel .dim-botCardTop { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
|
|
136
158
|
.dim-panel .dim-botIdentity { min-width: 0; display: flex; align-items: center; gap: 12px; }
|
|
137
159
|
.dim-panel .dim-botAvatar { flex: none; width: 42px; height: 42px; display: grid; place-items: center; overflow: hidden; border-radius: 12px; box-shadow: none; }
|
|
@@ -144,8 +166,8 @@ const CSS = String.raw`
|
|
|
144
166
|
.dim-panel .dim-botCard .dim-healthDot[data-tone="success"] { background: var(--dsw-alias-state-success-primary, #20a162); box-shadow: 0 0 0 3px color-mix(in srgb, var(--dsw-alias-state-success-primary, #20a162) 14%, transparent); }
|
|
145
167
|
.dim-panel .dim-botCard .dim-healthDot[data-tone="warning"] { background: var(--dsw-alias-state-warn-primary, #d97706); }
|
|
146
168
|
.dim-panel .dim-botCard .dim-healthDot[data-tone="error"] { background: var(--dsw-alias-state-error-primary, #d54941); }
|
|
147
|
-
.dim-panel .dim-botMetrics { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin:
|
|
148
|
-
.dim-panel .dim-botMetric { min-width: 0; padding:
|
|
169
|
+
.dim-panel .dim-botMetrics { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin: 8px 0; }
|
|
170
|
+
.dim-panel .dim-botMetric { min-width: 0; padding: 8px; border: 0; border-radius: 9px; background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
|
|
149
171
|
.dim-panel .dim-botMetric dt { margin: 0; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; font-weight: 400; line-height: normal; }
|
|
150
172
|
.dim-panel .dim-botMetric dd { overflow: hidden; margin: 5px 0 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 13px; font-weight: 400; line-height: normal; text-overflow: ellipsis; white-space: nowrap; }
|
|
151
173
|
.dim-panel .dim-botCard .dim-cardFooter { margin-top: 0; }
|
|
@@ -182,6 +204,7 @@ const CSS = String.raw`
|
|
|
182
204
|
@media (max-width: 560px) {
|
|
183
205
|
.dim-title { flex-direction: column; gap: 10px; }
|
|
184
206
|
.dim-title p { white-space: normal; }
|
|
207
|
+
.dim-githubTooltip { right: auto; left: 0; }
|
|
185
208
|
.dim-rail { grid-template-columns: minmax(0, 1fr); }
|
|
186
209
|
}
|
|
187
210
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import { h } from './i18n.js';
|
|
4
|
+
|
|
5
|
+
function looksLikeAbsolutePath(value) {
|
|
6
|
+
return value.startsWith('/') || /^[A-Za-z]:[\\/]/.test(value) || /^\\\\/.test(value);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function WorkspaceEditor({ workspace, disabled = false, onSave }) {
|
|
10
|
+
const [editing, setEditing] = React.useState(false);
|
|
11
|
+
const [draft, setDraft] = React.useState(workspace ?? '');
|
|
12
|
+
const [saving, setSaving] = React.useState(false);
|
|
13
|
+
const [error, setError] = React.useState(null);
|
|
14
|
+
const inputRef = React.useRef(null);
|
|
15
|
+
const editButtonRef = React.useRef(null);
|
|
16
|
+
const restoreFocus = React.useRef(false);
|
|
17
|
+
|
|
18
|
+
React.useEffect(() => {
|
|
19
|
+
if (!editing) setDraft(workspace ?? '');
|
|
20
|
+
}, [workspace, editing]);
|
|
21
|
+
|
|
22
|
+
React.useEffect(() => {
|
|
23
|
+
if (editing) inputRef.current?.focus();
|
|
24
|
+
else if (restoreFocus.current) {
|
|
25
|
+
restoreFocus.current = false;
|
|
26
|
+
editButtonRef.current?.focus();
|
|
27
|
+
}
|
|
28
|
+
}, [editing]);
|
|
29
|
+
|
|
30
|
+
const cancel = () => {
|
|
31
|
+
setDraft(workspace ?? '');
|
|
32
|
+
setError(null);
|
|
33
|
+
restoreFocus.current = true;
|
|
34
|
+
setEditing(false);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const submit = async (event) => {
|
|
38
|
+
event.preventDefault();
|
|
39
|
+
const value = draft.trim();
|
|
40
|
+
if (!value || saving || disabled) return;
|
|
41
|
+
if (!looksLikeAbsolutePath(value)) {
|
|
42
|
+
setError('工作区必须是绝对路径。');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
setSaving(true);
|
|
46
|
+
setError(null);
|
|
47
|
+
try {
|
|
48
|
+
await onSave?.(value);
|
|
49
|
+
restoreFocus.current = true;
|
|
50
|
+
setEditing(false);
|
|
51
|
+
} catch (cause) {
|
|
52
|
+
setError(cause?.message ?? '工作区修改失败,请重试。');
|
|
53
|
+
} finally {
|
|
54
|
+
setSaving(false);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return h('div', { className: 'dim-workspace' },
|
|
59
|
+
h('div', { className: 'dim-workspaceHeader' },
|
|
60
|
+
h('span', null, '当前工作区'),
|
|
61
|
+
editing ? null : h('button', {
|
|
62
|
+
type: 'button',
|
|
63
|
+
ref: editButtonRef,
|
|
64
|
+
className: 'dim-workspaceEdit',
|
|
65
|
+
onClick: () => { setEditing(true); setError(null); },
|
|
66
|
+
disabled,
|
|
67
|
+
}, '修改')),
|
|
68
|
+
editing
|
|
69
|
+
? h('form', { className: 'dim-workspaceForm', onSubmit: submit },
|
|
70
|
+
h('input', {
|
|
71
|
+
ref: inputRef,
|
|
72
|
+
value: draft,
|
|
73
|
+
onChange: (event) => setDraft(event.target.value),
|
|
74
|
+
placeholder: '/绝对路径/到/工作区',
|
|
75
|
+
'aria-label': '工作区绝对路径',
|
|
76
|
+
autoCapitalize: 'none',
|
|
77
|
+
autoCorrect: 'off',
|
|
78
|
+
spellCheck: false,
|
|
79
|
+
maxLength: 4_096,
|
|
80
|
+
required: true,
|
|
81
|
+
disabled: saving || disabled,
|
|
82
|
+
}),
|
|
83
|
+
h('div', { className: 'dim-workspaceActions' },
|
|
84
|
+
h('button', {
|
|
85
|
+
type: 'submit', disabled: saving || disabled || !draft.trim(),
|
|
86
|
+
}, saving ? '保存中…' : '保存'),
|
|
87
|
+
h('button', { type: 'button', onClick: cancel, disabled: saving || disabled }, '取消')),
|
|
88
|
+
error ? h('p', { className: 'dim-workspaceError', role: 'alert' }, error) : null)
|
|
89
|
+
: workspace
|
|
90
|
+
? React.createElement('code', {
|
|
91
|
+
className: 'dim-workspacePath',
|
|
92
|
+
title: workspace,
|
|
93
|
+
}, workspace)
|
|
94
|
+
: h('code', { className: 'dim-workspacePath' }, '未设置'),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/** Keep every full-snapshot response ordered behind the latest client mutation. */
|
|
4
|
+
export function useWorkspaceSnapshotFence() {
|
|
5
|
+
const state = React.useRef({ version: 0, pendingMutations: 0 });
|
|
6
|
+
return React.useMemo(() => Object.freeze({
|
|
7
|
+
beginStatus() {
|
|
8
|
+
return state.current.pendingMutations === 0 ? state.current.version : null;
|
|
9
|
+
},
|
|
10
|
+
canCommitStatus(version) {
|
|
11
|
+
return version !== null
|
|
12
|
+
&& state.current.pendingMutations === 0
|
|
13
|
+
&& state.current.version === version;
|
|
14
|
+
},
|
|
15
|
+
beginMutation() {
|
|
16
|
+
state.current.pendingMutations += 1;
|
|
17
|
+
state.current.version += 1;
|
|
18
|
+
return state.current.version;
|
|
19
|
+
},
|
|
20
|
+
canCommitMutation(version) {
|
|
21
|
+
return state.current.version === version;
|
|
22
|
+
},
|
|
23
|
+
endMutation() {
|
|
24
|
+
state.current.pendingMutations = Math.max(0, state.current.pendingMutations - 1);
|
|
25
|
+
return state.current.pendingMutations === 0;
|
|
26
|
+
},
|
|
27
|
+
}), []);
|
|
28
|
+
}
|
|
@@ -8,6 +8,12 @@ import { DingtalkController } from '../../../../src/channels/dingtalk/dingtalk-c
|
|
|
8
8
|
import { DingtalkRuntime } from '../../../../src/channels/dingtalk/dingtalk-runtime.mjs';
|
|
9
9
|
import { HarnessClient } from '../../../../src/channels/dingtalk/harness-client.mjs';
|
|
10
10
|
import { DingtalkStateStore } from '../../../../src/channels/dingtalk/state-store.mjs';
|
|
11
|
+
import {
|
|
12
|
+
BotWorkspaceStore,
|
|
13
|
+
createBotWorkspaceScope,
|
|
14
|
+
createWorkspaceAwareController,
|
|
15
|
+
observeBotWorkspaceRemovals,
|
|
16
|
+
} from '../../../../src/channels/shared/bot-workspace-store.mjs';
|
|
11
17
|
import { createConnectionSupervisor } from './connection-supervisor.mjs';
|
|
12
18
|
|
|
13
19
|
function harnessOrigin(webServer, configured) {
|
|
@@ -26,6 +32,7 @@ function pluginPaths(config) {
|
|
|
26
32
|
root,
|
|
27
33
|
config: resolve(config.configPath ?? join(root, 'config.json')),
|
|
28
34
|
bots: resolve(config.botsDir ?? join(root, 'bots')),
|
|
35
|
+
workspaces: resolve(config.workspacesPath ?? join(root, 'workspaces.json')),
|
|
29
36
|
};
|
|
30
37
|
}
|
|
31
38
|
|
|
@@ -45,6 +52,19 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
45
52
|
: (ctx.logger ?? console);
|
|
46
53
|
const paths = pluginPaths(config);
|
|
47
54
|
const configStore = await new ConfigStore(paths.config).load();
|
|
55
|
+
const defaultWorkspace = resolve(config.workspace ?? process.cwd());
|
|
56
|
+
const WorkspaceStore = internals.WorkspaceStore ?? BotWorkspaceStore;
|
|
57
|
+
const workspaces = internals.workspaces
|
|
58
|
+
?? await new WorkspaceStore(paths.workspaces, { defaultWorkspace }).load();
|
|
59
|
+
const canListConfiguredBots = typeof configStore.list === 'function';
|
|
60
|
+
const configuredBots = canListConfiguredBots ? configStore.list() : [];
|
|
61
|
+
if (canListConfiguredBots) {
|
|
62
|
+
await workspaces.reconcile(configuredBots.map((bot) => bot.botId));
|
|
63
|
+
}
|
|
64
|
+
await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.botId)));
|
|
65
|
+
const observedConfigStore = typeof configStore.remove === 'function'
|
|
66
|
+
? observeBotWorkspaceRemovals(configStore, { workspaces })
|
|
67
|
+
: configStore;
|
|
48
68
|
const deviceAuth = internals.deviceAuth ?? new DeviceAuth({
|
|
49
69
|
baseUrl: config.registrationBaseUrl,
|
|
50
70
|
});
|
|
@@ -61,23 +81,25 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
61
81
|
};
|
|
62
82
|
const harness = new Harness({
|
|
63
83
|
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
|
|
64
|
-
workspace:
|
|
84
|
+
workspace: defaultWorkspace,
|
|
65
85
|
agentPreset: config.agentPreset ?? 'standard',
|
|
66
86
|
autostart: false,
|
|
67
87
|
dshBin: config.dshBin ?? 'dsh',
|
|
68
88
|
});
|
|
69
|
-
const
|
|
89
|
+
const coreController = new Controller({
|
|
70
90
|
deviceAuth,
|
|
71
91
|
credentials: ctx.credentials,
|
|
72
|
-
configStore,
|
|
92
|
+
configStore: observedConfigStore,
|
|
73
93
|
logger,
|
|
74
94
|
createRuntime: async ({ botId, config: botConfig, clientSecret }) => {
|
|
75
95
|
const state = await stateFor(botId);
|
|
96
|
+
await workspaces.ensure(botId);
|
|
97
|
+
const workspaceScope = createBotWorkspaceScope(harness, { botId, workspaces, state });
|
|
76
98
|
return new Runtime({
|
|
77
99
|
config: botConfig,
|
|
78
100
|
clientSecret,
|
|
79
|
-
harness,
|
|
80
|
-
state,
|
|
101
|
+
harness: workspaceScope.harness,
|
|
102
|
+
state: workspaceScope.state,
|
|
81
103
|
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
|
|
82
104
|
maxMessageChars: config.maxMessageChars ?? 4_000,
|
|
83
105
|
connectTimeoutMs: config.connectTimeoutMs ?? 15_000,
|
|
@@ -94,15 +116,16 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
94
116
|
stateStores.delete(botId);
|
|
95
117
|
if (state && typeof state.remove === 'function') {
|
|
96
118
|
await state.remove();
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
119
|
+
} else {
|
|
120
|
+
try {
|
|
121
|
+
await unlink(statePath(botId));
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
124
|
+
}
|
|
103
125
|
}
|
|
104
126
|
},
|
|
105
127
|
});
|
|
128
|
+
const controller = createWorkspaceAwareController(coreController, { workspaces, stateFor });
|
|
106
129
|
const supervisor = createSupervisor({
|
|
107
130
|
controller,
|
|
108
131
|
harness,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import QRCode from 'qrcode';
|
|
2
2
|
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
|
3
|
+
import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
|
|
3
4
|
|
|
4
5
|
export const DINGTALK_RPC_CHANNEL = '/dingtalk';
|
|
5
6
|
export const DINGTALK_ENDPOINTS = Object.freeze({
|
|
@@ -10,6 +11,7 @@ export const DINGTALK_ENDPOINTS = Object.freeze({
|
|
|
10
11
|
bindCredentials: 'bot.bind-credentials',
|
|
11
12
|
reconnectBot: 'bot.reconnect',
|
|
12
13
|
deleteBot: 'bot.delete',
|
|
14
|
+
setWorkspace: SET_WORKSPACE_ENDPOINT,
|
|
13
15
|
approveSender: 'bot.sender.approve',
|
|
14
16
|
revokeSender: 'bot.sender.revoke',
|
|
15
17
|
});
|
|
@@ -76,6 +78,10 @@ function payloadFailure(endpoint, payload) {
|
|
|
76
78
|
? null
|
|
77
79
|
: 'bot.delete requires a botId and confirm=true.';
|
|
78
80
|
}
|
|
81
|
+
if (endpoint === DINGTALK_ENDPOINTS.setWorkspace) {
|
|
82
|
+
return validWorkspacePayload(payload)
|
|
83
|
+
? null : '请输入工作区绝对路径。';
|
|
84
|
+
}
|
|
79
85
|
if (endpoint === DINGTALK_ENDPOINTS.approveSender) {
|
|
80
86
|
return exactKeys(payload, ['botId', 'requestId', 'confirm'])
|
|
81
87
|
&& validId(payload.botId)
|
|
@@ -207,6 +213,12 @@ export function createDingtalkRpcHandler(controller, { encodeQr = qrDataUrl } =
|
|
|
207
213
|
value = await publicStatus(await controller.reconnectBot(payload.botId), cachedEncode);
|
|
208
214
|
} else if (endpoint === DINGTALK_ENDPOINTS.deleteBot) {
|
|
209
215
|
value = await publicStatus(await controller.deleteBot(payload.botId), cachedEncode);
|
|
216
|
+
} else if (endpoint === DINGTALK_ENDPOINTS.setWorkspace) {
|
|
217
|
+
if (typeof controller.updateWorkspace !== 'function') throw new Error('Workspace update is unavailable');
|
|
218
|
+
value = await publicStatus(
|
|
219
|
+
await controller.updateWorkspace(payload.botId, payload.workspace),
|
|
220
|
+
cachedEncode,
|
|
221
|
+
);
|
|
210
222
|
} else if (endpoint === DINGTALK_ENDPOINTS.approveSender) {
|
|
211
223
|
value = await publicStatus(
|
|
212
224
|
await controller.approveSender(payload.botId, payload.requestId),
|
|
@@ -219,8 +231,11 @@ export function createDingtalkRpcHandler(controller, { encodeQr = qrDataUrl } =
|
|
|
219
231
|
);
|
|
220
232
|
}
|
|
221
233
|
return signal?.aborted ? cancelled() : { ok: true, value };
|
|
222
|
-
} catch {
|
|
223
|
-
|
|
234
|
+
} catch (error) {
|
|
235
|
+
const workspaceError = publicWorkspaceError(error);
|
|
236
|
+
return signal?.aborted ? cancelled() : workspaceError
|
|
237
|
+
? { ok: false, error: workspaceError }
|
|
238
|
+
: internalFailure();
|
|
224
239
|
}
|
|
225
240
|
};
|
|
226
241
|
}
|
|
@@ -12,6 +12,12 @@ import {
|
|
|
12
12
|
} from '../../../../src/channels/feishu/plugin-config-store.mjs';
|
|
13
13
|
import { MultiBotDshFeishuController } from '../../../../src/channels/feishu/multi-bot-controller.mjs';
|
|
14
14
|
import { StateStore } from '../../../../src/channels/feishu/state-store.mjs';
|
|
15
|
+
import {
|
|
16
|
+
BotWorkspaceStore,
|
|
17
|
+
createBotWorkspaceScope,
|
|
18
|
+
createWorkspaceAwareController,
|
|
19
|
+
observeBotWorkspaceRemovals,
|
|
20
|
+
} from '../../../../src/channels/shared/bot-workspace-store.mjs';
|
|
15
21
|
|
|
16
22
|
function harnessOrigin(webServer, configured) {
|
|
17
23
|
if (configured !== undefined) return new URL(configured);
|
|
@@ -34,6 +40,7 @@ function pluginPaths(config) {
|
|
|
34
40
|
config: resolve(config.configPath ?? join(root, 'config.json')),
|
|
35
41
|
legacyState: resolve(config.statePath ?? join(root, 'state.json')),
|
|
36
42
|
bots: resolve(config.botsDir ?? join(root, 'bots')),
|
|
43
|
+
workspaces: resolve(config.workspacesPath ?? join(root, 'workspaces.json')),
|
|
37
44
|
};
|
|
38
45
|
}
|
|
39
46
|
|
|
@@ -58,6 +65,24 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
58
65
|
: (ctx.logger ?? console);
|
|
59
66
|
const paths = pluginPaths(config);
|
|
60
67
|
const configStore = await new ConfigStore(paths.config).load();
|
|
68
|
+
const defaultWorkspace = resolve(config.workspace ?? process.cwd());
|
|
69
|
+
const WorkspaceStore = internals.WorkspaceStore ?? BotWorkspaceStore;
|
|
70
|
+
const workspaces = internals.workspaces
|
|
71
|
+
?? await new WorkspaceStore(paths.workspaces, { defaultWorkspace }).load();
|
|
72
|
+
const canListConfiguredBots = typeof configStore.list === 'function';
|
|
73
|
+
const listConfiguredBots = () => canListConfiguredBots ? configStore.list() : [];
|
|
74
|
+
const configuredBots = listConfiguredBots();
|
|
75
|
+
if (canListConfiguredBots) {
|
|
76
|
+
await workspaces.reconcile(configuredBots.map((bot) => bot.id));
|
|
77
|
+
}
|
|
78
|
+
await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.id)));
|
|
79
|
+
const observedConfigStore = typeof configStore.removeBot === 'function'
|
|
80
|
+
? observeBotWorkspaceRemovals(configStore, {
|
|
81
|
+
workspaces,
|
|
82
|
+
method: 'removeBot',
|
|
83
|
+
botIdFromRemoved: (removed) => removed.id,
|
|
84
|
+
})
|
|
85
|
+
: configStore;
|
|
61
86
|
// State is lazy per bot. A corrupt legacy file can therefore fail only the
|
|
62
87
|
// migrated bot and cannot prevent healthy v2 bots from starting.
|
|
63
88
|
const stateStores = new Map();
|
|
@@ -75,9 +100,14 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
75
100
|
}
|
|
76
101
|
return state;
|
|
77
102
|
};
|
|
103
|
+
const stateForBotId = async (botId) => {
|
|
104
|
+
const botConfig = listConfiguredBots().find((bot) => bot.id === botId);
|
|
105
|
+
if (!botConfig) throw new Error('Unknown Feishu bot');
|
|
106
|
+
return stateFor(botConfig);
|
|
107
|
+
};
|
|
78
108
|
const harness = new Harness({
|
|
79
109
|
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
|
|
80
|
-
workspace:
|
|
110
|
+
workspace: defaultWorkspace,
|
|
81
111
|
agentPreset: config.agentPreset ?? 'standard',
|
|
82
112
|
// This plugin is already hosted by a running DSH process. Starting a
|
|
83
113
|
// second DSH would create a competing server and lifecycle.
|
|
@@ -85,21 +115,24 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
85
115
|
dshBin: config.dshBin ?? 'dsh',
|
|
86
116
|
});
|
|
87
117
|
|
|
88
|
-
const
|
|
118
|
+
const coreController = new Controller({
|
|
89
119
|
registerApp: (options) => lark.registerApp(options),
|
|
90
120
|
verifyApp,
|
|
91
121
|
credentials: ctx.credentials,
|
|
92
|
-
configStore,
|
|
122
|
+
configStore: observedConfigStore,
|
|
93
123
|
createRuntime: async ({ botId, config: botConfig, appSecret }) => {
|
|
94
124
|
const state = await stateFor(botConfig);
|
|
125
|
+
const id = botId ?? botConfig.id ?? botConfig.appId;
|
|
126
|
+
await workspaces.ensure(id);
|
|
127
|
+
const workspaceScope = createBotWorkspaceScope(harness, { botId: id, workspaces, state });
|
|
95
128
|
return new Runtime({
|
|
96
129
|
lark,
|
|
97
130
|
appId: botConfig.appId,
|
|
98
131
|
appSecret,
|
|
99
132
|
domain: botConfig.domain,
|
|
100
133
|
ownerOpenIds: botConfig.ownerOpenIds ?? [botConfig.ownerOpenId],
|
|
101
|
-
harness,
|
|
102
|
-
state,
|
|
134
|
+
harness: workspaceScope.harness,
|
|
135
|
+
state: workspaceScope.state,
|
|
103
136
|
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
|
|
104
137
|
logger: {
|
|
105
138
|
error: (...args) => logger.error?.(`[${botId ?? botConfig.id}]`, ...args),
|
|
@@ -118,6 +151,10 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
118
151
|
}
|
|
119
152
|
},
|
|
120
153
|
});
|
|
154
|
+
const controller = createWorkspaceAwareController(coreController, {
|
|
155
|
+
workspaces,
|
|
156
|
+
stateFor: stateForBotId,
|
|
157
|
+
});
|
|
121
158
|
|
|
122
159
|
const supervisor = createSupervisor({
|
|
123
160
|
controller,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import QRCode from 'qrcode';
|
|
2
2
|
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
|
3
|
+
import { publicWorkspaceError, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
|
|
3
4
|
import {
|
|
4
5
|
FEISHU_ENDPOINTS,
|
|
5
6
|
FEISHU_RPC_CHANNEL,
|
|
@@ -174,6 +175,7 @@ function publicBotEntry(entry) {
|
|
|
174
175
|
bot: publicBot(source.bot),
|
|
175
176
|
health: publicHealth(source, connected),
|
|
176
177
|
};
|
|
178
|
+
if (typeof source.workspace === 'string' && source.workspace) result.workspace = source.workspace;
|
|
177
179
|
const error = publicError(source.error);
|
|
178
180
|
if (error) result.error = error;
|
|
179
181
|
return result;
|
|
@@ -268,6 +270,10 @@ function validPayload(endpoint, payload) {
|
|
|
268
270
|
? null
|
|
269
271
|
: 'Deleting a bot requires a valid botId and confirm=true.';
|
|
270
272
|
}
|
|
273
|
+
if (endpoint === FEISHU_ENDPOINTS.setWorkspace) {
|
|
274
|
+
return validWorkspacePayload(payload)
|
|
275
|
+
? null : '请输入工作区绝对路径。';
|
|
276
|
+
}
|
|
271
277
|
return 'Unknown Feishu endpoint.';
|
|
272
278
|
}
|
|
273
279
|
|
|
@@ -429,14 +435,23 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
|
|
|
429
435
|
} else if (endpoint === FEISHU_MULTI_ENDPOINTS.disconnectBot) {
|
|
430
436
|
if (typeof controller.disconnectBot !== 'function') throw new Error('Multi-bot disconnect is unavailable');
|
|
431
437
|
value = await toPublicFeishuStatus(await controller.disconnectBot(payload.botId), { encodeQr: cachedEncodeQr });
|
|
438
|
+
} else if (endpoint === FEISHU_ENDPOINTS.setWorkspace) {
|
|
439
|
+
if (typeof controller.updateWorkspace !== 'function') throw new Error('Workspace update is unavailable');
|
|
440
|
+
value = await toPublicFeishuStatus(
|
|
441
|
+
await controller.updateWorkspace(payload.botId, payload.workspace),
|
|
442
|
+
{ encodeQr: cachedEncodeQr },
|
|
443
|
+
);
|
|
432
444
|
} else {
|
|
433
445
|
if (typeof controller.deleteBot !== 'function') throw new Error('Multi-bot delete is unavailable');
|
|
434
446
|
value = await toPublicFeishuStatus(await controller.deleteBot(payload.botId), { encodeQr: cachedEncodeQr });
|
|
435
447
|
}
|
|
436
448
|
if (signal?.aborted) return cancelled();
|
|
437
449
|
return { ok: true, value };
|
|
438
|
-
} catch {
|
|
439
|
-
|
|
450
|
+
} catch (error) {
|
|
451
|
+
const workspaceError = publicWorkspaceError(error);
|
|
452
|
+
return signal?.aborted ? cancelled() : workspaceError
|
|
453
|
+
? { ok: false, error: { ...workspaceError, details: {} } }
|
|
454
|
+
: internalFailure();
|
|
440
455
|
}
|
|
441
456
|
};
|
|
442
457
|
}
|