@dsh-workbench/module-tools 1.1.12 → 1.1.14-beta.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 +36 -23
- package/bin/dsh-wb-module.mjs +18 -4
- package/package.json +4 -4
- package/src/arch.mjs +36 -25
- package/src/check.mjs +50 -3
- package/src/checks/arch.mjs +2 -2
- package/src/checks/contracts.mjs +88 -41
- package/src/checks/kit.mjs +3 -2
- package/src/checks/lint.mjs +2 -2
- package/src/checks/primitives.mjs +2 -2
- package/src/discover.mjs +186 -16
- package/src/init.mjs +7 -8
- package/src/lint.mjs +4 -16
- package/src/market-index.mjs +1 -1
- package/src/migrate.mjs +69 -0
- package/src/release-journal.mjs +3 -1
- package/src/release-target.mjs +1 -1
- package/src/release-util.mjs +7 -1
- package/template-diy/AGENTS.md.tpl +14 -22
- package/template-diy/README.md.tpl +61 -65
- package/template-diy/design.json.tpl +19 -21
- package/template-diy/package.json.tpl +6 -2
- package/template-diy/scripts/smoke.assert.mjs.tpl +28 -0
- package/template-diy/scripts/smoke.mjs.tpl +150 -30
- package/template-diy/src/__tests__/plugin.test.ts.tpl +453 -0
- package/template-diy/src/components/NotesSection.tsx.tpl +74 -0
- package/template-diy/src/components/OverviewBody.module.css.tpl +5 -0
- package/template-diy/src/components/OverviewBody.tsx.tpl +64 -0
- package/template-diy/src/components/OverviewCard.module.css.tpl +9 -1
- package/template-diy/src/components/OverviewCard.tsx.tpl +108 -67
- package/template-diy/src/components/PrefsSection.tsx.tpl +36 -0
- package/template-diy/src/components/RunSection.tsx.tpl +69 -0
- package/template-diy/src/components/{OverviewCard.test.ts.tpl → __tests__/OverviewCard.test.ts.tpl} +51 -1
- package/template-diy/src/declared-tools.ts.tpl +6 -0
- package/template-diy/src/host/notes.ts.tpl +92 -0
- package/template-diy/src/host/overview.ts.tpl +57 -0
- package/template-diy/src/host/sample.ts.tpl +151 -0
- package/template-diy/src/host.ts.tpl +88 -66
- package/template-diy/src/messages.ts.tpl +28 -0
- package/template-diy/src/model/__tests__/flow.test.ts.tpl +36 -0
- package/template-diy/src/model/__tests__/notes.test.ts.tpl +49 -0
- package/template-diy/src/model/{overview.test.ts.tpl → __tests__/overview.test.ts.tpl} +1 -1
- package/template-diy/src/model/__tests__/prefs.test.ts.tpl +59 -0
- package/template-diy/src/model/flow.ts.tpl +47 -0
- package/template-diy/src/model/notes.ts.tpl +52 -0
- package/template-diy/src/model/overview.ts.tpl +3 -1
- package/template-diy/src/model/prefs.ts.tpl +61 -0
- package/template/AGENTS.md.tpl +0 -33
- package/template/CHANGELOG.md.tpl +0 -7
- package/template/README.md.tpl +0 -30
- package/template/arch.config.mjs +0 -2
- package/template/cordis.patch.yml.tpl +0 -3
- package/template/eslint.config.mjs +0 -2
- package/template/package.json.tpl +0 -50
- package/template/scripts/smoke.mjs.tpl +0 -192
- package/template/src/client.tsx.tpl +0 -22
- package/template/src/components/HelloCard.tsx.tpl +0 -28
- package/template/src/host.ts.tpl +0 -21
- package/template/src/messages.ts.tpl +0 -12
- package/template/src/plugin.test.ts.tpl +0 -56
- package/template/tsconfig.json +0 -20
- package/template/types/globals.d.ts +0 -14
- package/template/vitest.config.ts +0 -14
- package/template-diy/src/plugin.test.ts.tpl +0 -159
|
@@ -1,83 +1,74 @@
|
|
|
1
|
-
import { createElement as h } from 'react';
|
|
1
|
+
import { createElement as h, useState } from 'react';
|
|
2
2
|
import { kit } from '@dsh-workbench/core/client';
|
|
3
|
-
import {
|
|
3
|
+
import { overviewStateOf, type OverviewReady } from '../model/overview';
|
|
4
|
+
import { RUN_RESOURCE, type SampleRun } from '../model/flow';
|
|
5
|
+
import { NOTE_RESOURCE, NOTES_ROUTE, type Note } from '../model/notes';
|
|
6
|
+
import { LIMIT_DEFAULT, PREFS_RESOURCE, PREFS_ROUTE, type Prefs } from '../model/prefs';
|
|
7
|
+
import { NotesSection } from './NotesSection';
|
|
8
|
+
import { OverviewBody } from './OverviewBody';
|
|
9
|
+
import { PrefsSection } from './PrefsSection';
|
|
10
|
+
import { RunSection } from './RunSection';
|
|
4
11
|
import styles from './OverviewCard.module.css';
|
|
5
12
|
|
|
6
13
|
const UNIT_ID = '__UNIT__';
|
|
7
14
|
const OVERVIEW_ROUTE = `${UNIT_ID}/overview`;
|
|
8
15
|
|
|
9
|
-
const { Card
|
|
10
|
-
const { useApiResource } = kit.hooks;
|
|
16
|
+
const { Card } = kit.components;
|
|
17
|
+
const { useApiResource, useHostWrite, useLongTask } = kit.hooks;
|
|
11
18
|
|
|
12
|
-
|
|
13
|
-
|
|
19
|
+
interface NotesPayload {
|
|
20
|
+
readonly ok: boolean;
|
|
21
|
+
readonly notes?: readonly Note[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface PrefsPayload {
|
|
25
|
+
readonly ok: boolean;
|
|
26
|
+
readonly prefs?: Prefs;
|
|
27
|
+
}
|
|
14
28
|
|
|
15
29
|
export interface OverviewCardProps {
|
|
16
30
|
readonly path: string;
|
|
17
31
|
readonly refreshToken: number;
|
|
18
32
|
readonly onRefresh: () => void;
|
|
19
|
-
readonly t:
|
|
33
|
+
readonly t: (key: string) => string;
|
|
20
34
|
}
|
|
21
35
|
|
|
22
|
-
function
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
},
|
|
56
|
-
|
|
57
|
-
'div',
|
|
58
|
-
{ className: styles.largest },
|
|
59
|
-
h(
|
|
60
|
-
Rows,
|
|
61
|
-
null,
|
|
62
|
-
...payload.largest.map((file) =>
|
|
63
|
-
h(Row, {
|
|
64
|
-
key: file.name,
|
|
65
|
-
label: file.name,
|
|
66
|
-
value: `${formatBytes(file.bytes)} · ${file.mtime}`,
|
|
67
|
-
}),
|
|
68
|
-
),
|
|
69
|
-
),
|
|
70
|
-
),
|
|
71
|
-
);
|
|
72
|
-
}
|
|
36
|
+
export function OverviewCard(props: OverviewCardProps): ReturnType<typeof h> {
|
|
37
|
+
const [draft, setDraft] = useState('');
|
|
38
|
+
const [notesToken, setNotesToken] = useState(0);
|
|
39
|
+
const [prefsToken, setPrefsToken] = useState(0);
|
|
40
|
+
const path = props.path === '' ? undefined : props.path;
|
|
41
|
+
|
|
42
|
+
const prefsResource = useApiResource<PrefsPayload>(PREFS_ROUTE, { path }, prefsToken, {
|
|
43
|
+
enabled: props.path !== '',
|
|
44
|
+
});
|
|
45
|
+
const limit = prefsResource.value?.prefs?.limit ?? LIMIT_DEFAULT;
|
|
46
|
+
const resource = useApiResource<OverviewReady>(OVERVIEW_ROUTE, { path, limit }, props.refreshToken, {
|
|
47
|
+
enabled: props.path !== '',
|
|
48
|
+
});
|
|
49
|
+
const notesResource = useApiResource<NotesPayload>(NOTES_ROUTE, { path }, notesToken, {
|
|
50
|
+
enabled: props.path !== '',
|
|
51
|
+
});
|
|
52
|
+
const feed = useLongTask<SampleRun>({
|
|
53
|
+
unit: UNIT_ID,
|
|
54
|
+
workspace: props.path === '' ? null : props.path,
|
|
55
|
+
});
|
|
56
|
+
const flowWrite = useHostWrite<{ runId: string }>({
|
|
57
|
+
channel: 'actions',
|
|
58
|
+
resource: RUN_RESOURCE,
|
|
59
|
+
networkText: props.t(`${UNIT_ID}.network`),
|
|
60
|
+
});
|
|
61
|
+
const noteWrite = useHostWrite<{ notes?: readonly Note[] }>({
|
|
62
|
+
channel: 'actions',
|
|
63
|
+
resource: NOTE_RESOURCE,
|
|
64
|
+
networkText: props.t(`${UNIT_ID}.network`),
|
|
65
|
+
});
|
|
66
|
+
const prefsWrite = useHostWrite<Prefs>({
|
|
67
|
+
channel: 'prefs',
|
|
68
|
+
resource: PREFS_RESOURCE,
|
|
69
|
+
networkText: props.t(`${UNIT_ID}.network`),
|
|
70
|
+
});
|
|
73
71
|
|
|
74
|
-
export function OverviewCard(props: OverviewCardProps): Rendered {
|
|
75
|
-
const resource = useApiResource<OverviewReady>(
|
|
76
|
-
OVERVIEW_ROUTE,
|
|
77
|
-
{ path: props.path === '' ? undefined : props.path, limit: LIST_LIMIT },
|
|
78
|
-
props.refreshToken,
|
|
79
|
-
{ enabled: props.path !== '' },
|
|
80
|
-
);
|
|
81
72
|
const state = overviewStateOf({ phase: resource.phase, value: resource.value, error: resource.error });
|
|
82
73
|
return h(
|
|
83
74
|
Card,
|
|
@@ -85,8 +76,58 @@ export function OverviewCard(props: OverviewCardProps): Rendered {
|
|
|
85
76
|
t: props.t,
|
|
86
77
|
title: props.t(`${UNIT_ID}.cardTitle`),
|
|
87
78
|
refreshing: resource.phase === 'loading',
|
|
88
|
-
onRefresh:
|
|
79
|
+
onRefresh: () => {
|
|
80
|
+
props.onRefresh();
|
|
81
|
+
feed.refresh();
|
|
82
|
+
setNotesToken((token) => token + 1);
|
|
83
|
+
setPrefsToken((token) => token + 1);
|
|
84
|
+
},
|
|
89
85
|
},
|
|
90
|
-
|
|
86
|
+
h(OverviewBody, { state, t: props.t }),
|
|
87
|
+
h(RunSection, {
|
|
88
|
+
path: props.path,
|
|
89
|
+
className: styles.block,
|
|
90
|
+
t: props.t,
|
|
91
|
+
feed,
|
|
92
|
+
write: flowWrite,
|
|
93
|
+
failed: flowWrite.failed,
|
|
94
|
+
}),
|
|
95
|
+
h(NotesSection, {
|
|
96
|
+
path: props.path,
|
|
97
|
+
className: styles.block,
|
|
98
|
+
lineClassName: styles.line,
|
|
99
|
+
t: props.t,
|
|
100
|
+
notes: notesResource.value?.notes ?? [],
|
|
101
|
+
draft,
|
|
102
|
+
onDraft: setDraft,
|
|
103
|
+
write: {
|
|
104
|
+
busy: noteWrite.busy,
|
|
105
|
+
failed: noteWrite.failed,
|
|
106
|
+
send: async (text: string) => {
|
|
107
|
+
const outcome = await noteWrite.send({ path: props.path, text });
|
|
108
|
+
if (outcome.ok) {
|
|
109
|
+
setDraft('');
|
|
110
|
+
setNotesToken((token) => token + 1);
|
|
111
|
+
props.onRefresh();
|
|
112
|
+
}
|
|
113
|
+
return outcome;
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
}),
|
|
117
|
+
h(PrefsSection, {
|
|
118
|
+
path: props.path,
|
|
119
|
+
t: props.t,
|
|
120
|
+
limit,
|
|
121
|
+
busy: prefsWrite.busy,
|
|
122
|
+
failed: prefsWrite.failed,
|
|
123
|
+
onChange: (next: number) => {
|
|
124
|
+
void prefsWrite.send({ workspace: props.path, limit: next }).then((outcome) => {
|
|
125
|
+
if (outcome.ok) {
|
|
126
|
+
setPrefsToken((token) => token + 1);
|
|
127
|
+
props.onRefresh();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
}),
|
|
91
132
|
);
|
|
92
133
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createElement as h } from 'react';
|
|
2
|
+
import { kit } from '@dsh-workbench/core/client';
|
|
3
|
+
import { LIMIT_CHOICES } from '../model/prefs';
|
|
4
|
+
|
|
5
|
+
const { Field, Note, SelectField } = kit.components;
|
|
6
|
+
const UNIT_ID = '__UNIT__';
|
|
7
|
+
|
|
8
|
+
export interface PrefsSectionProps {
|
|
9
|
+
readonly path: string;
|
|
10
|
+
readonly t: (key: string) => string;
|
|
11
|
+
readonly limit: number;
|
|
12
|
+
readonly busy: boolean;
|
|
13
|
+
readonly failed: string | null;
|
|
14
|
+
readonly onChange: (limit: number) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function PrefsSection({ path, t, limit, busy, failed, onChange }: PrefsSectionProps): ReturnType<typeof h> {
|
|
18
|
+
const known = LIMIT_CHOICES.includes(limit as (typeof LIMIT_CHOICES)[number]);
|
|
19
|
+
const choices = known ? [...LIMIT_CHOICES] : [...LIMIT_CHOICES, limit].sort((a, b) => a - b);
|
|
20
|
+
return h(
|
|
21
|
+
Field,
|
|
22
|
+
{ label: t(`${UNIT_ID}.prefsLabel`), hint: t(`${UNIT_ID}.prefsHint`) },
|
|
23
|
+
failed === null ? null : h(Note, null, failed),
|
|
24
|
+
h(SelectField, {
|
|
25
|
+
value: String(limit),
|
|
26
|
+
disabled: path === '' || busy,
|
|
27
|
+
options: choices.map((choice) => ({
|
|
28
|
+
value: String(choice),
|
|
29
|
+
label: `${String(choice)}${t(`${UNIT_ID}.prefsUnit`)}`,
|
|
30
|
+
})),
|
|
31
|
+
onChange: (value: string) => {
|
|
32
|
+
onChange(Number(value));
|
|
33
|
+
},
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createElement as h } from 'react';
|
|
2
|
+
import { Button } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
3
|
+
import { kit } from '@dsh-workbench/core/client';
|
|
4
|
+
import { headlineOf, runIsLive, type SampleRun } from '../model/flow';
|
|
5
|
+
|
|
6
|
+
const { LongTaskRow, Note } = kit.components;
|
|
7
|
+
const UNIT_ID = '__UNIT__';
|
|
8
|
+
|
|
9
|
+
export interface RunSectionProps {
|
|
10
|
+
readonly path: string;
|
|
11
|
+
readonly className: string;
|
|
12
|
+
readonly t: (key: string) => string;
|
|
13
|
+
readonly feed: {
|
|
14
|
+
readonly runs: readonly SampleRun[];
|
|
15
|
+
stop(id: string): Promise<unknown>;
|
|
16
|
+
readonly stopping: string | null;
|
|
17
|
+
readonly stopFailed: string | null;
|
|
18
|
+
refresh(): void;
|
|
19
|
+
};
|
|
20
|
+
readonly write: { readonly busy: boolean; send(body: Record<string, unknown>): Promise<{ readonly ok: boolean }> };
|
|
21
|
+
readonly failed: string | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function RunSection({ path, className, t, feed, write, failed }: RunSectionProps): ReturnType<typeof h> {
|
|
25
|
+
const run = feed.runs[0];
|
|
26
|
+
const live = run !== undefined && runIsLive(run);
|
|
27
|
+
const stop =
|
|
28
|
+
run === undefined || !live
|
|
29
|
+
? undefined
|
|
30
|
+
: {
|
|
31
|
+
onStop: () => {
|
|
32
|
+
void feed.stop(run.id);
|
|
33
|
+
},
|
|
34
|
+
label: t(`${UNIT_ID}.stopLabel`),
|
|
35
|
+
askLabel: t(`${UNIT_ID}.stopAsk`),
|
|
36
|
+
busy: feed.stopping === run.id,
|
|
37
|
+
failed: feed.stopFailed,
|
|
38
|
+
};
|
|
39
|
+
return h(
|
|
40
|
+
'div',
|
|
41
|
+
{ className },
|
|
42
|
+
h(
|
|
43
|
+
Button,
|
|
44
|
+
{
|
|
45
|
+
variant: 'ghost',
|
|
46
|
+
size: 'sm',
|
|
47
|
+
disabled: path === '' || write.busy,
|
|
48
|
+
onClick: () => {
|
|
49
|
+
void write.send({ path }).then((outcome) => {
|
|
50
|
+
if (outcome.ok) feed.refresh();
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
t(`${UNIT_ID}.runSample`),
|
|
55
|
+
),
|
|
56
|
+
failed === null ? null : h(Note, null, failed),
|
|
57
|
+
run === undefined
|
|
58
|
+
? h(Note, null, t(`${UNIT_ID}.runHint`))
|
|
59
|
+
: h(LongTaskRow, {
|
|
60
|
+
status: run.status,
|
|
61
|
+
headline: headlineOf(run),
|
|
62
|
+
problem: run.status === 'failed' ? run.why : undefined,
|
|
63
|
+
steps: run.steps,
|
|
64
|
+
raw: run.why,
|
|
65
|
+
stepsTitle: t(`${UNIT_ID}.stepsTitle`),
|
|
66
|
+
stop,
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
}
|
package/template-diy/src/components/{OverviewCard.test.ts.tpl → __tests__/OverviewCard.test.ts.tpl}
RENAMED
|
@@ -7,8 +7,20 @@ const shell = vi.hoisted(() => ({
|
|
|
7
7
|
readonly error?: { readonly code: string; readonly message: string };
|
|
8
8
|
},
|
|
9
9
|
readEnabled: undefined as boolean | undefined,
|
|
10
|
+
writeFailed: null as string | null,
|
|
10
11
|
}));
|
|
11
12
|
|
|
13
|
+
vi.mock('react', async (importOriginal) => {
|
|
14
|
+
const actual = await importOriginal<typeof import('react')>();
|
|
15
|
+
return {
|
|
16
|
+
...actual,
|
|
17
|
+
useState: (initial: unknown) => [
|
|
18
|
+
typeof initial === 'function' ? (initial as () => unknown)() : initial,
|
|
19
|
+
() => {},
|
|
20
|
+
],
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
|
|
12
24
|
vi.mock('@dsh-workbench/core/client', () => ({
|
|
13
25
|
kit: {
|
|
14
26
|
hooks: {
|
|
@@ -16,20 +28,44 @@ vi.mock('@dsh-workbench/core/client', () => ({
|
|
|
16
28
|
shell.readEnabled = options?.enabled;
|
|
17
29
|
return shell.resource;
|
|
18
30
|
},
|
|
31
|
+
useHostWrite: () => ({
|
|
32
|
+
busy: false,
|
|
33
|
+
failed: shell.writeFailed,
|
|
34
|
+
detail: null,
|
|
35
|
+
send: async () => ({ ok: true, value: {} }),
|
|
36
|
+
fail: () => {},
|
|
37
|
+
clear: () => {},
|
|
38
|
+
}),
|
|
39
|
+
useLongTask: () => ({
|
|
40
|
+
runs: [],
|
|
41
|
+
stop: async () => ({ ok: true }),
|
|
42
|
+
stopping: null,
|
|
43
|
+
stopFailed: null,
|
|
44
|
+
refresh: () => {},
|
|
45
|
+
}),
|
|
19
46
|
},
|
|
20
47
|
components: {
|
|
21
48
|
Card: () => null,
|
|
22
49
|
CenteredState: () => null,
|
|
23
50
|
EmptyState: () => null,
|
|
51
|
+
Field: () => null,
|
|
24
52
|
StateBlock: () => null,
|
|
25
53
|
IconPanelScreen: () => null,
|
|
26
54
|
Rows: () => null,
|
|
27
55
|
Row: () => null,
|
|
56
|
+
LongTaskRow: () => null,
|
|
57
|
+
Note: () => null,
|
|
58
|
+
SelectField: () => null,
|
|
59
|
+
TextField: () => null,
|
|
28
60
|
},
|
|
29
61
|
},
|
|
30
62
|
}));
|
|
31
63
|
|
|
32
|
-
|
|
64
|
+
vi.mock('@deepseek-ai/dsh-client-ui-primitives', () => ({
|
|
65
|
+
Button: () => null,
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
import { OverviewCard } from '../OverviewCard';
|
|
33
69
|
|
|
34
70
|
function textOf(value: unknown, out: string[] = []): string[] {
|
|
35
71
|
if (typeof value === 'string') {
|
|
@@ -47,6 +83,10 @@ function textOf(value: unknown, out: string[] = []): string[] {
|
|
|
47
83
|
}
|
|
48
84
|
if (typeof value === 'object') {
|
|
49
85
|
const record = value as Record<string, unknown>;
|
|
86
|
+
if (typeof record['type'] === 'function' && record['props'] !== undefined) {
|
|
87
|
+
const rendered = (record['type'] as (props: unknown) => unknown)(record['props']);
|
|
88
|
+
if (rendered !== null && rendered !== undefined) return textOf(rendered, out);
|
|
89
|
+
}
|
|
50
90
|
const source = 'type' in record && 'props' in record ? (record.props as Record<string, unknown>) : record;
|
|
51
91
|
for (const one of Object.values(source)) {
|
|
52
92
|
if (typeof one !== 'function') textOf(one, out);
|
|
@@ -115,4 +155,14 @@ describe('OverviewCard:待机 / 加载 / 就绪 / 就绪但空 / 出错', () =
|
|
|
115
155
|
expect(text).toContain('/real/workbench');
|
|
116
156
|
expect(text).not.toContain('__UNIT__.loading');
|
|
117
157
|
});
|
|
158
|
+
|
|
159
|
+
it('写失败留在卡上(不静默)', () => {
|
|
160
|
+
shell.resource = {
|
|
161
|
+
phase: 'ready',
|
|
162
|
+
value: { ok: true, path: '/real/workbench', files: 1, dirs: 0, bytes: 10, largest: [] },
|
|
163
|
+
};
|
|
164
|
+
shell.writeFailed = '网络不通,没发出去';
|
|
165
|
+
expect(textOf(OverviewCard(cardProps)).join(' ')).toContain('网络不通,没发出去');
|
|
166
|
+
shell.writeFailed = null;
|
|
167
|
+
});
|
|
118
168
|
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import type { DshToolDefinition, UnitMountDeps } from '@dsh-workbench/core/contract';
|
|
3
|
+
import { TEXT_OUTPUT, recordOf, sessionWorkspace, textOf } from '@dsh-workbench/shared/host';
|
|
4
|
+
import { DECLARED_TOOLS } from '../declared-tools';
|
|
5
|
+
import {
|
|
6
|
+
NOTE_FILE,
|
|
7
|
+
UNIT_ID,
|
|
8
|
+
addNoteOf,
|
|
9
|
+
noteIdOf,
|
|
10
|
+
noteProblemOf,
|
|
11
|
+
notesOf,
|
|
12
|
+
type Note,
|
|
13
|
+
} from '../model/notes';
|
|
14
|
+
|
|
15
|
+
export interface NotesResult {
|
|
16
|
+
readonly ok: boolean;
|
|
17
|
+
readonly notes?: readonly Note[];
|
|
18
|
+
readonly note?: Note;
|
|
19
|
+
readonly code?: string;
|
|
20
|
+
readonly message?: string;
|
|
21
|
+
readonly text?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface Notes {
|
|
25
|
+
list(workspace: string): Note[];
|
|
26
|
+
add(workspace: string, rawText: unknown): NotesResult;
|
|
27
|
+
readonly tool: DshToolDefinition;
|
|
28
|
+
handle(payload: unknown): unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createNotes(deps: UnitMountDeps): Notes {
|
|
32
|
+
const storeAt = (workspace: string) => deps.store.for(UNIT_ID, workspace);
|
|
33
|
+
|
|
34
|
+
const list = (workspace: string): Note[] => notesOf(storeAt(workspace).read(NOTE_FILE));
|
|
35
|
+
|
|
36
|
+
const add = (workspace: string, rawText: unknown): NotesResult => {
|
|
37
|
+
const problem = noteProblemOf(rawText);
|
|
38
|
+
if (problem !== undefined) return { ok: false, code: 'note-invalid', message: problem };
|
|
39
|
+
const at = new Date().toISOString();
|
|
40
|
+
const existing = list(workspace);
|
|
41
|
+
const { note, notes } = addNoteOf(
|
|
42
|
+
existing,
|
|
43
|
+
String(rawText),
|
|
44
|
+
at,
|
|
45
|
+
noteIdOf(at, randomUUID().slice(0, 8)),
|
|
46
|
+
);
|
|
47
|
+
storeAt(workspace).write(NOTE_FILE, notes);
|
|
48
|
+
return { ok: true, note, notes };
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const tool: DshToolDefinition = {
|
|
52
|
+
name: DECLARED_TOOLS.addNote.name,
|
|
53
|
+
description: '往当前工作区的模块笔记里加一条(最多 20 条,只写本模块自己的文件夹,不动平台)。',
|
|
54
|
+
parameters: {
|
|
55
|
+
type: 'object',
|
|
56
|
+
properties: {
|
|
57
|
+
text: { type: 'string', description: '要记的一句话(200 字以内)。' },
|
|
58
|
+
},
|
|
59
|
+
required: ['text'],
|
|
60
|
+
additionalProperties: false,
|
|
61
|
+
},
|
|
62
|
+
output: TEXT_OUTPUT,
|
|
63
|
+
async execute(raw, exec) {
|
|
64
|
+
const workspace = sessionWorkspace(exec);
|
|
65
|
+
if (workspace === null || workspace === '') {
|
|
66
|
+
return { ok: false, text: '这个会话没有工作区:先打开一个项目再记。' };
|
|
67
|
+
}
|
|
68
|
+
const result = add(workspace, recordOf(raw)['text']);
|
|
69
|
+
return result.ok
|
|
70
|
+
? { ok: true, text: `记下了:${result.note?.text ?? ''}` }
|
|
71
|
+
: { ok: false, text: result.message ?? '没记上' };
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
list,
|
|
77
|
+
add,
|
|
78
|
+
tool,
|
|
79
|
+
handle(payload: unknown): unknown {
|
|
80
|
+
const body = recordOf(payload);
|
|
81
|
+
const workspace = textOf(body['path']);
|
|
82
|
+
if (workspace === '') {
|
|
83
|
+
return { ok: false, error: { code: 'no-workspace', message: '先打开一个项目再记' } };
|
|
84
|
+
}
|
|
85
|
+
const result = add(workspace, body['text']);
|
|
86
|
+
if (!result.ok) {
|
|
87
|
+
return { ok: false, error: { code: result.code ?? 'note-invalid', message: result.message ?? '没记上' } };
|
|
88
|
+
}
|
|
89
|
+
return { ok: true, notes: result.notes, text: `记下了:${result.note?.text ?? ''}` };
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import type { OverviewFile, OverviewPayload } from '../model/overview';
|
|
4
|
+
import { LIMIT_DEFAULT, LIMIT_MAX } from '../model/prefs';
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_LIMIT = LIMIT_DEFAULT;
|
|
7
|
+
export const MAX_LIMIT = LIMIT_MAX;
|
|
8
|
+
|
|
9
|
+
export function readOverview(path: string, limit: number): OverviewPayload {
|
|
10
|
+
const entries = readdirSync(path, { withFileTypes: true });
|
|
11
|
+
const files: OverviewFile[] = [];
|
|
12
|
+
let dirs = 0;
|
|
13
|
+
let bytes = 0;
|
|
14
|
+
for (const entry of entries) {
|
|
15
|
+
if (entry.isDirectory()) {
|
|
16
|
+
dirs += 1;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (!entry.isFile()) continue;
|
|
20
|
+
const info = statSync(join(path, entry.name));
|
|
21
|
+
bytes += info.size;
|
|
22
|
+
files.push({ name: entry.name, bytes: info.size, mtime: new Date(info.mtimeMs).toISOString() });
|
|
23
|
+
}
|
|
24
|
+
files.sort((a, b) => b.bytes - a.bytes || a.name.localeCompare(b.name));
|
|
25
|
+
return { path, files: files.length, dirs, bytes, largest: files.slice(0, limit) };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function searchParam(search: URLSearchParams, name: string): string | undefined {
|
|
29
|
+
const raw = search.get(name);
|
|
30
|
+
return raw === null || raw === '' ? undefined : raw;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function limitOf(search: URLSearchParams): number {
|
|
34
|
+
const raw = searchParam(search, 'limit');
|
|
35
|
+
if (raw === undefined) return DEFAULT_LIMIT;
|
|
36
|
+
const parsed = Number(raw);
|
|
37
|
+
if (!Number.isFinite(parsed)) return DEFAULT_LIMIT;
|
|
38
|
+
return Math.min(MAX_LIMIT, Math.max(1, Math.trunc(parsed)));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function overviewAnswer(search: URLSearchParams): unknown {
|
|
42
|
+
const target = searchParam(search, 'path');
|
|
43
|
+
if (target === undefined) {
|
|
44
|
+
return { ok: false, error: { code: 'no-path', message: '没告诉我要看哪个目录' } };
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
return { ok: true, ...readOverview(target, limitOf(search)) };
|
|
48
|
+
} catch (error) {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
error: {
|
|
52
|
+
code: 'overview-unreadable',
|
|
53
|
+
message: error instanceof Error ? error.message : String(error),
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|