dsh-my-computer 0.0.0-stage → 0.1.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/package.json CHANGED
@@ -1,6 +1,58 @@
1
1
  {
2
2
  "name": "dsh-my-computer",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "DSH Web 右栏「我的电脑」:远程浏览与操作整台 Windows 磁盘(跨盘符、不受会话工作区限制)",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./client": "./lib/client.js",
10
+ "./cordis.patch.yml": "./cordis.patch.yml",
11
+ "./package.json": "./package.json"
12
+ },
13
+ "files": [
14
+ "lib",
15
+ "tools",
16
+ "cordis.patch.yml",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "keywords": [
21
+ "deepseek-harness",
22
+ "dsh",
23
+ "dsh-plugin",
24
+ "file-manager",
25
+ "windows",
26
+ "remote"
27
+ ],
28
+ "author": "ymf2317",
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/ymf2317-tech/dsh-my-computer.git"
33
+ },
34
+ "homepage": "https://github.com/ymf2317-tech/dsh-my-computer#readme",
35
+ "bugs": {
36
+ "url": "https://github.com/ymf2317-tech/dsh-my-computer/issues"
37
+ },
38
+ "engines": {
39
+ "node": ">=22.5.0",
40
+ "dsh": ">=0.2.2-rc.1 <0.3.0-0"
41
+ },
42
+ "scripts": {
43
+ "smoke": "node tools/smoke.mjs"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "dsh": {
49
+ "bundle": {
50
+ "patch": "./cordis.patch.yml"
51
+ },
52
+ "client": {
53
+ "platform": "web",
54
+ "inject": []
55
+ },
56
+ "manifestVersion": 1
57
+ }
58
+ }
@@ -0,0 +1,408 @@
1
+ #!/usr/bin/env node
2
+ // dsh-my-computer 客户端 bundle 自检
3
+ //
4
+ // 这个脚本**不能**证明 DSH 会真的把面板渲染出来(那只有装进 profile、重启 dsh web、
5
+ // 刷新页面才算),但它能在没有浏览器的前提下验证最容易写错的那几件事:
6
+ // 1. lib/client.js 能被解析(语法没坏);
7
+ // 2. 它调用的是 window.__ModuleLoader__.load({ id, factory }) 这个约定;
8
+ // 3. factory 返回的模块里有 apply / inject,apply 是函数;
9
+ // 4. apply(ctx) 注册了 sidebar.panellist(id=my-computer, order=-5, label=我的电脑)
10
+ // 和 main(key=my-computer),两个组件都是函数;
11
+ // 5. 面板能渲染:空目录态、有内容的列表态、删除确认弹窗、预览弹窗
12
+ // (用 React 真身 + 手搭的最小 hook dispatcher;不执行 useEffect,因此不发请求)。
13
+ //
14
+ // 跑法:node tools/client-check.mjs
15
+
16
+ import fs from 'node:fs'
17
+ import path from 'node:path'
18
+ import vm from 'node:vm'
19
+ import { createRequire } from 'node:module'
20
+ import { fileURLToPath } from 'node:url'
21
+
22
+ const HERE = path.dirname(fileURLToPath(import.meta.url))
23
+ const CLIENT = path.resolve(HERE, '../lib/client.js')
24
+
25
+ const checks = []
26
+ function check(name, pass, detail) {
27
+ checks.push({ name, pass: Boolean(pass), detail })
28
+ console.log((pass ? ' ✓' : ' ✗') + ' ' + name + (detail ? ' ' + detail : ''))
29
+ return Boolean(pass)
30
+ }
31
+ function section(title) {
32
+ console.log('\n── ' + title + ' ' + '─'.repeat(Math.max(0, 56 - title.length)))
33
+ }
34
+
35
+ console.log('dsh-my-computer 客户端 bundle 自检')
36
+ console.log(' 文件:' + CLIENT)
37
+
38
+ const source = fs.readFileSync(CLIENT, 'utf8')
39
+
40
+ section('bundle 形状')
41
+
42
+ try {
43
+ new vm.Script(source, { filename: CLIENT })
44
+ check('bundle 语法可解析', true, source.length + ' 字节')
45
+ } catch (error) {
46
+ check('bundle 语法可解析', false, String(error))
47
+ }
48
+
49
+ const requireFrom = createRequire(path.resolve(HERE, '../package.json'))
50
+ let React = null
51
+ let reactFrom = ''
52
+ for (const candidate of ['C:/Users/Administrator/.dsh/profiles/web/node_modules/react', 'react']) {
53
+ try {
54
+ React = requireFrom(candidate)
55
+ reactFrom = candidate
56
+ break
57
+ } catch {
58
+ /* 换下一个 */
59
+ }
60
+ }
61
+ check('找到 react 运行时', Boolean(React), reactFrom || '未找到(渲染检查会跳过)')
62
+
63
+ const loaded = []
64
+ const styleNodes = []
65
+ const documentStub = {
66
+ querySelector() {
67
+ return null
68
+ },
69
+ createElement() {
70
+ const node = {
71
+ attributes: {},
72
+ textContent: '',
73
+ setAttribute(name, value) {
74
+ this.attributes[name] = value
75
+ },
76
+ remove() {
77
+ const at = styleNodes.indexOf(node)
78
+ if (at >= 0) styleNodes.splice(at, 1)
79
+ },
80
+ }
81
+ return node
82
+ },
83
+ head: {
84
+ appendChild(node) {
85
+ styleNodes.push(node)
86
+ },
87
+ },
88
+ body: { appendChild() {} },
89
+ }
90
+ const windowStub = {
91
+ __ModuleLoader__: {
92
+ load(entry) {
93
+ loaded.push(entry)
94
+ },
95
+ },
96
+ }
97
+
98
+ let bundleError = null
99
+ try {
100
+ const run = new Function('window', 'document', 'require', 'globalThis', source)
101
+ run(
102
+ windowStub,
103
+ documentStub,
104
+ (name) => {
105
+ if (name === 'react' && React) return React
106
+ throw new Error('宿主要求了一个这里没有的模块:' + name)
107
+ },
108
+ globalThis,
109
+ )
110
+ } catch (error) {
111
+ bundleError = error
112
+ }
113
+ check('bundle 可以执行(没抛异常)', bundleError === null, bundleError ? String(bundleError) : '')
114
+ check('通过 __ModuleLoader__.load 注册了自己', loaded.length === 1, loaded.length + ' 次')
115
+
116
+ const entry = loaded[0] || {}
117
+ check('模块 id 是 dsh-my-computer', entry.id === 'dsh-my-computer', String(entry.id))
118
+ check('factory 是函数', typeof entry.factory === 'function')
119
+
120
+ const mod = entry.factory
121
+ ? entry.factory((name) => {
122
+ if (name === 'react' && React) return React
123
+ throw new Error('未知模块:' + name)
124
+ })
125
+ : {}
126
+
127
+ check('exports.apply 是函数', typeof mod.apply === 'function')
128
+ check('exports.inject 是数组', Array.isArray(mod.inject), JSON.stringify(mod.inject))
129
+
130
+ section('apply(ctx) 注册的 slots')
131
+
132
+ const registrations = []
133
+ const fakeClientCtx = {
134
+ effect(fn) {
135
+ const dispose = fn()
136
+ return typeof dispose === 'function' ? dispose : () => {}
137
+ },
138
+ slots: {
139
+ inject(name, callback) {
140
+ return callback()
141
+ },
142
+ register(spec, component) {
143
+ registrations.push({ spec, component })
144
+ return () => {}
145
+ },
146
+ },
147
+ }
148
+
149
+ let applyError = null
150
+ try {
151
+ mod.apply(fakeClientCtx)
152
+ } catch (error) {
153
+ applyError = error
154
+ }
155
+ check('apply(ctx) 没抛异常', applyError === null, applyError ? String(applyError) : '')
156
+ check('注册了 2 个 slot', registrations.length === 2, registrations.map((r) => r.spec && r.spec.name).join(', '))
157
+ check('注入了 <style>(样式跟随主题)', styleNodes.length === 1, styleNodes.length + ' 个 style 节点')
158
+
159
+ const side = registrations.find((r) => r.spec && r.spec.name === 'sidebar.panellist')
160
+ const main = registrations.find((r) => r.spec && r.spec.name === 'main')
161
+
162
+ check('有 sidebar.panellist 注册', Boolean(side))
163
+ check('侧栏 id 是 my-computer', side && side.spec.id === 'my-computer', side ? String(side.spec.id) : '')
164
+ check('侧栏 order 是 -5', side && side.spec.order === -5, side ? String(side.spec.order) : '')
165
+ check(
166
+ '侧栏 label 返回「我的电脑」',
167
+ side && typeof side.spec.label === 'function' && side.spec.label() === '我的电脑',
168
+ side && typeof side.spec.label === 'function' ? side.spec.label() : '',
169
+ )
170
+ check('侧栏图标是函数组件', side && typeof side.component === 'function')
171
+
172
+ check('有 main 注册', Boolean(main))
173
+ check('main.key 与侧栏 id 一致', main && side && main.spec.key === side.spec.id, main ? String(main.spec.key) : '')
174
+ check('面板是函数组件', main && typeof main.component === 'function')
175
+
176
+ /* ------------------------------------------------------------------ *
177
+ * 手搭一个只够跑首屏的渲染器:真 React.createElement + 最小 hook dispatcher
178
+ * ------------------------------------------------------------------ */
179
+
180
+ function makeRenderer(React) {
181
+ const internals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
182
+ const dispatcherRef = internals && internals.ReactCurrentDispatcher
183
+ if (!dispatcherRef) return null
184
+
185
+ return function render(element, options = {}) {
186
+ const seen = { tags: [], classes: [], components: [] }
187
+ const seed = options.seed || null
188
+ let seededOnce = false
189
+
190
+ function walk(node, depth) {
191
+ if (node == null || typeof node === 'boolean') return
192
+ if (typeof node === 'string' || typeof node === 'number') return
193
+ if (Array.isArray(node)) {
194
+ node.forEach((child) => walk(child, depth))
195
+ return
196
+ }
197
+ if (typeof node.type === 'string') {
198
+ seen.tags.push(node.type)
199
+ const className = node.props && node.props.className
200
+ if (className) seen.classes.push(String(className))
201
+ walk(node.props && node.props.children, depth + 1)
202
+ return
203
+ }
204
+ if (typeof node.type === 'function') {
205
+ if (depth > 14) throw new Error('渲染层级过深(疑似递归组件)')
206
+ const name = node.type.name || '匿名组件'
207
+ seen.components.push(name)
208
+ const store = []
209
+ const useSeed = seed && !seededOnce
210
+ if (useSeed) seededOnce = true
211
+ let index = 0
212
+ const previous = dispatcherRef.current
213
+ dispatcherRef.current = {
214
+ useState(initial) {
215
+ const at = index
216
+ index += 1
217
+ if (!(at in store)) {
218
+ const preset = useSeed && seed[at] !== undefined ? seed[at] : undefined
219
+ store[at] = preset !== undefined ? preset : typeof initial === 'function' ? initial() : initial
220
+ }
221
+ return [
222
+ store[at],
223
+ (next) => {
224
+ store[at] = typeof next === 'function' ? next(store[at]) : next
225
+ },
226
+ ]
227
+ },
228
+ useReducer(reducer, initial) {
229
+ const at = index
230
+ index += 1
231
+ if (!(at in store)) store[at] = initial
232
+ return [store[at], () => {}]
233
+ },
234
+ useRef(initial) {
235
+ const at = index
236
+ index += 1
237
+ if (!(at in store)) store[at] = { current: initial }
238
+ return store[at]
239
+ },
240
+ useMemo(factory) {
241
+ index += 1
242
+ return factory()
243
+ },
244
+ useCallback(callback) {
245
+ index += 1
246
+ return callback
247
+ },
248
+ useEffect() {
249
+ index += 1
250
+ },
251
+ useLayoutEffect() {
252
+ index += 1
253
+ },
254
+ useContext(context) {
255
+ index += 1
256
+ return context && context._currentValue
257
+ },
258
+ useId() {
259
+ index += 1
260
+ return 'id-' + index
261
+ },
262
+ }
263
+ try {
264
+ walk(node.type(node.props || {}), depth + 1)
265
+ } finally {
266
+ dispatcherRef.current = previous
267
+ }
268
+ return
269
+ }
270
+ throw new Error('未知节点类型:' + String(node.type))
271
+ }
272
+
273
+ walk(element, 0)
274
+ return seen
275
+ }
276
+ }
277
+
278
+ section('面板渲染(四个状态)')
279
+
280
+ if (React && main && typeof main.component === 'function') {
281
+ const render = makeRenderer(React)
282
+ if (!render) {
283
+ console.log(' · 跳过:这个 React 版本没有暴露 ReactCurrentDispatcher')
284
+ } else {
285
+ const driveA = { letter: 'C:', label: '系统盘', total: 100 * 1024 ** 3, free: 30 * 1024 ** 3 }
286
+ const driveB = { letter: 'D:', label: '本地磁盘', total: 500 * 1024 ** 3, free: 210 * 1024 ** 3 }
287
+ const entries = [
288
+ { name: 'dsh-workspace', path: 'D:\\dsh-workspace', kind: 'dir', size: 0, mtime: Date.now(), ext: '' },
289
+ { name: 'vault', path: 'D:\\dsh-workspace\\vault', kind: 'dir', size: 0, mtime: Date.now(), ext: '' },
290
+ { name: 'README.md', path: 'D:\\dsh-workspace\\README.md', kind: 'file', size: 4096, mtime: Date.now(), ext: '.md' },
291
+ { name: '快捷方式', path: 'D:\\link', kind: 'link', targetKind: 'dir', size: 0, mtime: Date.now(), ext: '' },
292
+ ]
293
+ // 与 MyComputerPanel 的 hook 顺序一一对应:
294
+ // cwd, entries, drives, capacity, loading, error, notice, selected, query, pathInput, dialog, busy
295
+ const baseSeed = (dialog) => [
296
+ 'D:\\dsh-workspace',
297
+ entries,
298
+ [driveA, driveB],
299
+ { total: driveB.total, free: driveB.free },
300
+ false,
301
+ '',
302
+ '',
303
+ [],
304
+ '',
305
+ 'D:\\dsh-workspace',
306
+ dialog || null,
307
+ '',
308
+ ]
309
+
310
+ // A) 空状态(刚打开、还没探测到磁盘)
311
+ let empty = null
312
+ let emptyError = null
313
+ try {
314
+ empty = render(React.createElement(main.component, {}), {
315
+ seed: ['', [], [], { total: null, free: null }, true, '', '', [], '', '', null, ''],
316
+ })
317
+ } catch (error) {
318
+ emptyError = error
319
+ }
320
+ check('空状态渲染没抛异常', emptyError === null, emptyError ? String(emptyError) : '')
321
+ check('空状态有根容器 mc-root', empty && empty.classes.some((c) => c.indexOf('mc-root') === 0))
322
+ check('空状态显示「加载中」', empty && empty.classes.some((c) => c === 'mc-empty'))
323
+
324
+ // B) 有内容的列表
325
+ let list = null
326
+ let listError = null
327
+ try {
328
+ list = render(React.createElement(main.component, {}), { seed: baseSeed(null) })
329
+ } catch (error) {
330
+ listError = error
331
+ }
332
+ check('列表态渲染没抛异常', listError === null, listError ? String(listError) : '')
333
+ const rowCount = list ? list.classes.filter((c) => c === 'mc-row').length : 0
334
+ check('目录里的 4 个条目都渲染成行', rowCount === 4, rowCount + ' 行')
335
+ check('切盘区渲染出 2 个盘符按钮', list && list.classes.filter((c) => c.indexOf('mc-chip') === 0).length === 2)
336
+ check('面包屑渲染出来', list && list.classes.some((c) => c === 'mc-crumb' || c === 'mc-crumb last'))
337
+ check('工具栏有新建/上传按钮', list && list.tags.filter((t) => t === 'button').length >= 4)
338
+ check('行内操作按钮存在(改名/删除/下载)', list && list.classes.includes('mc-ibtn') && list.classes.includes('mc-ibtn danger'))
339
+
340
+ // C) 删除确认弹窗
341
+ let del = null
342
+ let delError = null
343
+ try {
344
+ del = render(React.createElement(main.component, {}), {
345
+ seed: baseSeed({ kind: 'delete', paths: ['D:\\dsh-workspace\\README.md'] }),
346
+ })
347
+ } catch (error) {
348
+ delError = error
349
+ }
350
+ check('删除确认弹窗渲染没抛异常', delError === null, delError ? String(delError) : '')
351
+ check('弹窗有遮罩与主体', del && del.classes.includes('mc-modal-bg') && del.classes.some((c) => c.indexOf('mc-modal') === 0))
352
+ check('弹窗里有「永久删除」按钮', del && del.components.indexOf('ModalShell') >= 0)
353
+
354
+ // D) 预览弹窗(图片直显)
355
+ let img = null
356
+ let imgError = null
357
+ try {
358
+ img = render(React.createElement(main.component, {}), {
359
+ seed: baseSeed({
360
+ kind: 'preview',
361
+ path: 'D:\\shot.png',
362
+ name: 'shot.png',
363
+ size: 20480,
364
+ group: 'image',
365
+ text: null,
366
+ loaded: false,
367
+ loadError: '',
368
+ truncated: false,
369
+ encoding: '',
370
+ }),
371
+ })
372
+ } catch (error) {
373
+ imgError = error
374
+ }
375
+ check('图片预览渲染没抛异常', imgError === null, imgError ? String(imgError) : '')
376
+ check('图片预览用了 <img>', img && img.tags.includes('img'))
377
+ check('图片指向 /api/my-computer/download', img && img.components.length > 0)
378
+ }
379
+ } else {
380
+ console.log(' · 跳过渲染检查(没有 react 或没有拿到组件)')
381
+ }
382
+
383
+ section('侧栏图标')
384
+
385
+ if (React && side && typeof side.component === 'function') {
386
+ let icon = null
387
+ let iconError = null
388
+ try {
389
+ icon = side.component({})
390
+ check('图标渲染出 svg', icon && icon.type === 'svg')
391
+ check('图标 viewBox 24×24', icon && icon.props && icon.props.viewBox === '0 0 24 24')
392
+ } catch (error) {
393
+ iconError = error
394
+ }
395
+ check('图标渲染没抛异常', iconError === null, iconError ? String(iconError) : '')
396
+ }
397
+
398
+ const passed = checks.filter((c) => c.pass).length
399
+ const total = checks.length
400
+ console.log('\n' + '='.repeat(64))
401
+ console.log(`结果:${passed}/${total} 通过,${total - passed} 失败`)
402
+ console.log('='.repeat(64))
403
+ if (passed !== total) {
404
+ for (const c of checks) if (!c.pass) console.log(' ✗ ' + c.name + (c.detail ? ' ' + c.detail : ''))
405
+ process.exitCode = 1
406
+ } else {
407
+ console.log('全部通过。注意:这不等于 DSH 里已经能看见面板——还要装进 profile 并重启 dsh web。')
408
+ }
@@ -0,0 +1,118 @@
1
+ // 检查客户端半区在真实 ctx 形状下会注册哪些东西(不渲染,只用假 ctx 捕获注册)
2
+ import { readFileSync } from 'node:fs'
3
+
4
+ const src = readFileSync(new URL('../lib/client.js', import.meta.url), 'utf8')
5
+
6
+ let registration
7
+ const fakeWindow = {
8
+ __ModuleLoader__: {
9
+ load(reg) {
10
+ registration = reg
11
+ },
12
+ },
13
+ }
14
+ const fakeDocument = {
15
+ head: { appendChild() {} },
16
+ createElement() {
17
+ return { setAttribute() {}, appendChild() {}, style: {}, textContent: '' }
18
+ },
19
+ querySelector() {
20
+ return null
21
+ },
22
+ getElementById() {
23
+ return null
24
+ },
25
+ }
26
+ globalThis.window = fakeWindow
27
+ globalThis.document = fakeDocument
28
+
29
+ new Function('window', 'document', src)(fakeWindow, fakeDocument)
30
+
31
+ if (!registration) throw new Error('没有捕获到 __ModuleLoader__.load 注册')
32
+ console.log('module id =', registration.id)
33
+
34
+ // react 桩:apply 阶段只注册组件,不真的渲染
35
+ const fakeReact = {
36
+ createElement: (type, props, ...kids) => ({ type, props, kids }),
37
+ Fragment: 'Fragment',
38
+ useState: (v) => [typeof v === 'function' ? v() : v, () => {}],
39
+ useEffect: () => {},
40
+ useMemo: (f) => f(),
41
+ useCallback: (f) => f,
42
+ useRef: (v) => ({ current: v }),
43
+ }
44
+ const clientPlugin = registration.factory((spec) => {
45
+ if (spec === 'react') return fakeReact
46
+ if (spec === 'react/jsx-runtime') return { jsx: fakeReact.createElement, jsxs: fakeReact.createElement }
47
+ throw new Error('unexpected external: ' + spec)
48
+ })
49
+
50
+ console.log('exports:', Object.keys(clientPlugin).join(', '))
51
+ console.log('inject:', JSON.stringify(clientPlugin.inject))
52
+
53
+ const seen = []
54
+ function makeSlots() {
55
+ return {
56
+ inject(name, cb) {
57
+ seen.push('slots.inject:' + name)
58
+ const dispose = cb()
59
+ return typeof dispose === 'function' ? dispose : () => {}
60
+ },
61
+ register(opts, comp) {
62
+ seen.push('slots.register:' + opts.name + (opts.key ? '[key=' + opts.key + ']' : opts.id ? '[id=' + opts.id + ']' : ''))
63
+ if (typeof comp !== 'function' && comp !== null) throw new Error('component 不是函数: ' + opts.name)
64
+ return () => {}
65
+ },
66
+ }
67
+ }
68
+
69
+ const disposers = []
70
+ const ctx = {
71
+ slots: makeSlots(),
72
+ effect(fn) {
73
+ const d = fn()
74
+ if (typeof d === 'function') disposers.push(d)
75
+ seen.push('effect')
76
+ },
77
+ inject(deps, cb) {
78
+ seen.push('ctx.inject:' + deps.join('+'))
79
+ const child = {
80
+ slots: ctx.slots,
81
+ sidebarRightTabs: {
82
+ register(def) {
83
+ seen.push(
84
+ 'sidebarRightTabs.register(id=' + def.id + ', kind=' + def.kind + ', guide=' + (def.guide ? def.guide.length : 0) + ')',
85
+ )
86
+ if (!def.id || !def.kind) throw new Error('id/kind 缺失')
87
+ if (typeof def.title !== 'function') throw new Error('title 必须是函数')
88
+ return () => {}
89
+ },
90
+ },
91
+ }
92
+ const d = cb(child)
93
+ if (typeof d === 'function') disposers.push(d)
94
+ },
95
+ }
96
+
97
+ clientPlugin.apply(ctx)
98
+
99
+ console.log('\n--- 注册轨迹 ---')
100
+ for (const s of seen) console.log(' ' + s)
101
+ console.log('\n__MYCOMP_ERR =', globalThis.window.__MYCOMP_ERR || '(无错误)')
102
+
103
+ const must = [
104
+ 'sidebarRightTabs.register(id=my-computer',
105
+ 'slots.inject:sidebar.right.pane.tab',
106
+ 'slots.register:sidebar.right.pane.tab[key=my-computer]',
107
+ 'slots.inject:sidebar.right.pane.tab.title',
108
+ 'slots.register:sidebar.right.pane.tab.title[key=my-computer]',
109
+ ]
110
+ let bad = 0
111
+ console.log('\n--- 断言 ---')
112
+ for (const m of must) {
113
+ const ok = seen.some((s) => s.startsWith(m.replace('[key=my-computer]', '')) || s === m || s.startsWith(m))
114
+ console.log((ok ? ' ✓ ' : ' ✗ ') + m)
115
+ if (!ok) bad++
116
+ }
117
+ console.log(bad === 0 ? '\n全部通过' : '\n失败 ' + bad + ' 项')
118
+ process.exit(bad === 0 ? 0 : 1)