@khorsheed/dsh-library 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.en.md +49 -0
- package/README.i18n.yaml +6 -0
- package/README.md +48 -2
- package/cordis.patch.yml +17 -0
- package/lib/client.js +66334 -0
- package/lib/index.js +3318 -0
- package/lib/profiles-CFrPQdfO.js +1692 -0
- package/lib/tool.js +1597 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +3811 -0
- package/lib/typert.remote-client.d.ts +137 -0
- package/lib/typert.remote-client.js +3077 -0
- package/lib/types/cards.d.ts +41 -0
- package/lib/types/cards.js +111 -0
- package/lib/types/client/LibraryPane.d.ts +26 -0
- package/lib/types/client/LibraryPane.js +129 -0
- package/lib/types/client/bind/BindChip.d.ts +33 -0
- package/lib/types/client/bind/BindChip.js +102 -0
- package/lib/types/client/contract.d.ts +167 -0
- package/lib/types/client/contract.js +1 -0
- package/lib/types/client/covers.d.ts +36 -0
- package/lib/types/client/covers.js +66 -0
- package/lib/types/client/definition.d.ts +27 -0
- package/lib/types/client/definition.js +32 -0
- package/lib/types/client/icons-local.d.ts +41 -0
- package/lib/types/client/icons-local.js +25 -0
- package/lib/types/client/icons.d.ts +27 -0
- package/lib/types/client/icons.js +11 -0
- package/lib/types/client/index.d.ts +35 -0
- package/lib/types/client/index.js +231 -0
- package/lib/types/client/judge.d.ts +70 -0
- package/lib/types/client/judge.js +262 -0
- package/lib/types/client/locales.d.ts +553 -0
- package/lib/types/client/locales.js +1076 -0
- package/lib/types/client/look.d.ts +63 -0
- package/lib/types/client/look.js +64 -0
- package/lib/types/client/math.d.ts +24 -0
- package/lib/types/client/math.js +47 -0
- package/lib/types/client/mistakes.d.ts +21 -0
- package/lib/types/client/mistakes.js +28 -0
- package/lib/types/client/preset-visibility.d.ts +109 -0
- package/lib/types/client/preset-visibility.js +123 -0
- package/lib/types/client/quiz-drafts.d.ts +29 -0
- package/lib/types/client/quiz-drafts.js +50 -0
- package/lib/types/client/reveal.d.ts +21 -0
- package/lib/types/client/reveal.js +31 -0
- package/lib/types/client/store.d.ts +111 -0
- package/lib/types/client/store.js +48 -0
- package/lib/types/client/titles.d.ts +17 -0
- package/lib/types/client/titles.js +23 -0
- package/lib/types/client/ui.d.ts +96 -0
- package/lib/types/client/ui.js +124 -0
- package/lib/types/client/views/book.d.ts +57 -0
- package/lib/types/client/views/book.js +164 -0
- package/lib/types/client/views/cards.d.ts +17 -0
- package/lib/types/client/views/cards.js +53 -0
- package/lib/types/client/views/home.d.ts +26 -0
- package/lib/types/client/views/home.js +263 -0
- package/lib/types/client/views/lesson.d.ts +42 -0
- package/lib/types/client/views/lesson.js +146 -0
- package/lib/types/client/views/path.d.ts +32 -0
- package/lib/types/client/views/path.js +64 -0
- package/lib/types/client/views/quiz.d.ts +42 -0
- package/lib/types/client/views/quiz.js +366 -0
- package/lib/types/client/views/station-quiz.d.ts +43 -0
- package/lib/types/client/views/station-quiz.js +203 -0
- package/lib/types/client/views/station.d.ts +18 -0
- package/lib/types/client/views/station.js +142 -0
- package/lib/types/image-refs.d.ts +35 -0
- package/lib/types/image-refs.js +71 -0
- package/lib/types/index.d.ts +51 -0
- package/lib/types/index.js +31 -0
- package/lib/types/mastery.d.ts +81 -0
- package/lib/types/mastery.js +110 -0
- package/lib/types/profiles.d.ts +31 -0
- package/lib/types/profiles.js +25 -0
- package/lib/types/remote.d.ts +355 -0
- package/lib/types/remote.js +332 -0
- package/lib/types/service/assess.d.ts +144 -0
- package/lib/types/service/assess.js +773 -0
- package/lib/types/service/bindings.d.ts +92 -0
- package/lib/types/service/bindings.js +198 -0
- package/lib/types/service/cards.d.ts +85 -0
- package/lib/types/service/cards.js +320 -0
- package/lib/types/service/core.d.ts +124 -0
- package/lib/types/service/core.js +169 -0
- package/lib/types/service/pages.d.ts +60 -0
- package/lib/types/service/pages.js +125 -0
- package/lib/types/service/roadmap.d.ts +99 -0
- package/lib/types/service/roadmap.js +284 -0
- package/lib/types/service/rules.d.ts +63 -0
- package/lib/types/service/rules.js +245 -0
- package/lib/types/service/shelf.d.ts +136 -0
- package/lib/types/service/shelf.js +422 -0
- package/lib/types/service.d.ts +42 -0
- package/lib/types/service.js +41 -0
- package/lib/types/store.d.ts +257 -0
- package/lib/types/store.js +1376 -0
- package/lib/types/tool-definition.d.ts +49 -0
- package/lib/types/tool-definition.js +49 -0
- package/lib/types/tool.d.ts +71 -0
- package/lib/types/tool.js +174 -0
- package/lib/types/tools/assess.d.ts +11 -0
- package/lib/types/tools/assess.js +82 -0
- package/lib/types/tools/binding.d.ts +11 -0
- package/lib/types/tools/binding.js +46 -0
- package/lib/types/tools/cards.d.ts +11 -0
- package/lib/types/tools/cards.js +100 -0
- package/lib/types/tools/kit.d.ts +215 -0
- package/lib/types/tools/kit.js +281 -0
- package/lib/types/tools/notes.d.ts +11 -0
- package/lib/types/tools/notes.js +40 -0
- package/lib/types/tools/page.d.ts +11 -0
- package/lib/types/tools/page.js +59 -0
- package/lib/types/tools/project.d.ts +11 -0
- package/lib/types/tools/project.js +67 -0
- package/lib/types/tools/quiz.d.ts +11 -0
- package/lib/types/tools/quiz.js +74 -0
- package/lib/types/tools/read.d.ts +11 -0
- package/lib/types/tools/read.js +127 -0
- package/lib/types/tools/resource.d.ts +11 -0
- package/lib/types/tools/resource.js +147 -0
- package/lib/types/tools/roadmap.d.ts +11 -0
- package/lib/types/tools/roadmap.js +102 -0
- package/lib/types/types.d.ts +752 -0
- package/lib/types/types.js +159 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +135 -3
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge cards: the dedupe write path, listing, editing, undo, review scheduling.
|
|
3
|
+
*
|
|
4
|
+
* One layer of the library service (see `service/core.ts` for the layering):
|
|
5
|
+
* the methods are the service's own, moved here verbatim by domain.
|
|
6
|
+
*
|
|
7
|
+
* @module @khorsheed/dsh-library/service/cards
|
|
8
|
+
*/
|
|
9
|
+
import { MAX_BOX, } from "../types.js";
|
|
10
|
+
import { cardKey, cardKeys, cardMatchesQuery, exactMatch, nearMatches, } from "../cards.js";
|
|
11
|
+
import { mintLibraryId } from "../store.js";
|
|
12
|
+
import { refuse, leitnerDueAt, clampWeakness, live } from "./core.js";
|
|
13
|
+
import { LibraryServiceShelf } from "./shelf.js";
|
|
14
|
+
export class LibraryServiceCards extends LibraryServiceShelf {
|
|
15
|
+
/* --------------------------------------------------------------- cards */
|
|
16
|
+
/**
|
|
17
|
+
* Apply card inputs to the project's cards (call inside the project lock).
|
|
18
|
+
* Exact keys merge; near matches are held back unless `mergeNear` (photo
|
|
19
|
+
* words — a plural or a case away is the same word) or the input decides.
|
|
20
|
+
*/
|
|
21
|
+
applyCardInputs(projectId, inputs, defaultResourceId, options = {}) {
|
|
22
|
+
const cards = this.store.readCards(projectId);
|
|
23
|
+
const byId = new Map(cards.map(card => [card.id, card]));
|
|
24
|
+
const order = cards.map(card => card.id);
|
|
25
|
+
const added = [];
|
|
26
|
+
const merged = [];
|
|
27
|
+
const pending = [];
|
|
28
|
+
const now = new Date().toISOString();
|
|
29
|
+
for (const [index, input] of inputs.entries()) {
|
|
30
|
+
const keys = [input.term, ...(input.aliases ?? [])].map(cardKey).filter(key => key !== '');
|
|
31
|
+
const pool = [...byId.values()];
|
|
32
|
+
let target;
|
|
33
|
+
if (input.mergeInto !== undefined) {
|
|
34
|
+
target = byId.get(input.mergeInto);
|
|
35
|
+
}
|
|
36
|
+
else if (input.asNew !== true) {
|
|
37
|
+
target = exactMatch(pool, keys);
|
|
38
|
+
if (target === undefined) {
|
|
39
|
+
const near = nearMatches(pool, keys);
|
|
40
|
+
if (near.length > 0) {
|
|
41
|
+
if (options.mergeNear === true) {
|
|
42
|
+
target = near[0];
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
pending.push({ index, term: input.term, candidates: near.map(card => ({ id: card.id, term: card.term, gloss: card.gloss })) });
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const resourceId = input.resourceId ?? defaultResourceId;
|
|
52
|
+
const source = {
|
|
53
|
+
type: input.source.type,
|
|
54
|
+
...(input.source.ref === undefined ? {} : { ref: input.source.ref }),
|
|
55
|
+
...(input.source.note === undefined || input.source.note.trim() === '' ? {} : { note: input.source.note.trim() }),
|
|
56
|
+
...(input.weaken === true ? { weakened: true } : {}),
|
|
57
|
+
at: now,
|
|
58
|
+
};
|
|
59
|
+
const context = input.context === undefined || input.context.sentence.trim() === ''
|
|
60
|
+
? undefined
|
|
61
|
+
: {
|
|
62
|
+
sentence: input.context.sentence.trim(),
|
|
63
|
+
...(resourceId === undefined ? {} : { resourceId }),
|
|
64
|
+
...(input.context.pageId === undefined ? {} : { pageId: input.context.pageId }),
|
|
65
|
+
...(input.context.nodeId === undefined ? {} : { nodeId: input.context.nodeId }),
|
|
66
|
+
};
|
|
67
|
+
if (target !== undefined) {
|
|
68
|
+
const aliases = new Set(target.aliases);
|
|
69
|
+
const ownKeys = new Set(cardKeys(target));
|
|
70
|
+
if (!ownKeys.has(cardKey(input.term)))
|
|
71
|
+
aliases.add(input.term.trim());
|
|
72
|
+
for (const alias of input.aliases ?? [])
|
|
73
|
+
if (!ownKeys.has(cardKey(alias)))
|
|
74
|
+
aliases.add(alias.trim());
|
|
75
|
+
const placeholder = target.gloss === '(待补释义)';
|
|
76
|
+
const next = {
|
|
77
|
+
...target,
|
|
78
|
+
aliases: [...aliases],
|
|
79
|
+
gloss: placeholder ? input.gloss.trim() : target.gloss,
|
|
80
|
+
...(target.userGloss === undefined && input.userGloss !== undefined ? { userGloss: input.userGloss.trim() } : {}),
|
|
81
|
+
contexts: context === undefined || target.contexts.some(existing => existing.sentence === context.sentence)
|
|
82
|
+
? target.contexts
|
|
83
|
+
: [...target.contexts, context],
|
|
84
|
+
...(target.resourceId === undefined && resourceId !== undefined ? { resourceId } : {}),
|
|
85
|
+
sources: [...target.sources, source],
|
|
86
|
+
weakness: clampWeakness(target.weakness + (input.weaken === true ? 1 : 0)),
|
|
87
|
+
// A card met again is active again — a re-sighting never resets mastery.
|
|
88
|
+
status: target.status === 'deleted' ? 'active' : target.status,
|
|
89
|
+
updatedAt: now,
|
|
90
|
+
};
|
|
91
|
+
byId.set(next.id, next);
|
|
92
|
+
merged.push(next);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const card = {
|
|
96
|
+
id: mintLibraryId('c'),
|
|
97
|
+
kind: input.kind,
|
|
98
|
+
term: input.term.trim(),
|
|
99
|
+
aliases: (input.aliases ?? []).map(alias => alias.trim()).filter(alias => alias !== '' && cardKey(alias) !== cardKey(input.term)),
|
|
100
|
+
gloss: input.gloss.trim(),
|
|
101
|
+
...(input.userGloss === undefined || input.userGloss.trim() === '' ? {} : { userGloss: input.userGloss.trim() }),
|
|
102
|
+
contexts: context === undefined ? [] : [context],
|
|
103
|
+
...(resourceId === undefined ? {} : { resourceId }),
|
|
104
|
+
sources: [source],
|
|
105
|
+
weakness: input.weaken === true || input.source.type === 'chat' ? 1 : 0,
|
|
106
|
+
mastery: { box: 0, dueAt: now, streak: 0 },
|
|
107
|
+
status: 'active',
|
|
108
|
+
createdAt: now,
|
|
109
|
+
updatedAt: now,
|
|
110
|
+
};
|
|
111
|
+
byId.set(card.id, card);
|
|
112
|
+
order.push(card.id);
|
|
113
|
+
added.push(card);
|
|
114
|
+
}
|
|
115
|
+
if (added.length > 0 || merged.length > 0) {
|
|
116
|
+
this.store.writeCards(projectId, order.map(cardId => byId.get(cardId)));
|
|
117
|
+
const touched = [...added, ...merged];
|
|
118
|
+
const sourceType = inputs[0]?.source.type;
|
|
119
|
+
this.event(projectId, 'cards.recorded', {
|
|
120
|
+
at: now,
|
|
121
|
+
title: touched.map(card => card.term).join('、').slice(0, 300),
|
|
122
|
+
ids: touched.map(card => card.id),
|
|
123
|
+
...(sourceType === undefined ? {} : { source: sourceType }),
|
|
124
|
+
...(sourceType === 'chat' && inputs[0]?.source.ref !== undefined ? { session: inputs[0].source.ref } : {}),
|
|
125
|
+
...(defaultResourceId === undefined ? {} : { resourceId: defaultResourceId }),
|
|
126
|
+
count: added.length,
|
|
127
|
+
count2: merged.length,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return { added, merged, pending, at: now };
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Record cards (the coach's 「记下」, a manual add). Near-duplicates come back
|
|
134
|
+
* in `pending` with their candidates — resend each with `mergeInto` or `asNew`.
|
|
135
|
+
*/
|
|
136
|
+
async writeCards(request) {
|
|
137
|
+
this.assertAlive('writeCards');
|
|
138
|
+
const project = this.readProjectOrRefusal(request.projectId);
|
|
139
|
+
if ('error' in project)
|
|
140
|
+
return project;
|
|
141
|
+
if (request.cards.length === 0) {
|
|
142
|
+
return refuse('invalid', 'cards needs at least one card — each: { kind: word|concept, term, gloss, context?, source: { type: chat|photo|quiz|manual, note? }, weaken? }');
|
|
143
|
+
}
|
|
144
|
+
// Per-card problems are collected and reported at once: a batch with
|
|
145
|
+
// three bad cards earns one refusal listing all three, not three
|
|
146
|
+
// fail-fast round trips.
|
|
147
|
+
const errors = [];
|
|
148
|
+
for (const [index, input] of request.cards.entries()) {
|
|
149
|
+
if (input.kind !== 'word' && input.kind !== 'concept')
|
|
150
|
+
errors.push(`cards[${index}].kind is word | concept`);
|
|
151
|
+
if (input.term.trim() === '' || input.gloss.trim() === '') {
|
|
152
|
+
errors.push(`cards[${index}] needs term and gloss — the gloss is the in-context meaning (a word) or a one-line explanation (a concept)`);
|
|
153
|
+
}
|
|
154
|
+
if (!['photo', 'chat', 'quiz', 'manual'].includes(input.source?.type)) {
|
|
155
|
+
errors.push(`cards[${index}].source.type is chat | photo | quiz | manual — chat: the learner asked / got stuck in conversation`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const cards = this.store.readCards(project.value.id);
|
|
159
|
+
for (const [index, input] of request.cards.entries()) {
|
|
160
|
+
if (input.mergeInto !== undefined && !cards.some(card => card.id === input.mergeInto && live(card))) {
|
|
161
|
+
errors.push(`cards[${index}].mergeInto names ${JSON.stringify(input.mergeInto)}, which is not a live card — use an id from the pending candidates`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (errors.length === 1)
|
|
165
|
+
return refuse('invalid', errors[0]);
|
|
166
|
+
if (errors.length > 1) {
|
|
167
|
+
return refuse('invalid', `${errors.length} problems in cards — fix them all, then resend:\n${errors.map(error => `- ${error}`).join('\n')}`);
|
|
168
|
+
}
|
|
169
|
+
if (request.resourceId !== undefined && this.store.readResource(project.value.id, request.resourceId) === undefined) {
|
|
170
|
+
return refuse('not-found', `no resource ${JSON.stringify(request.resourceId)} in this project — omit resource for a project-level card`);
|
|
171
|
+
}
|
|
172
|
+
return await this.mutate(project.value.id, () => {
|
|
173
|
+
const { at: _at, ...result } = this.applyCardInputs(project.value.id, request.cards, request.resourceId);
|
|
174
|
+
return { ok: true, ...result };
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
/** Cards, filtered and sorted (the sidebar list, the coach's search). */
|
|
178
|
+
async listCards(request) {
|
|
179
|
+
this.assertAlive('listCards');
|
|
180
|
+
const now = new Date().toISOString();
|
|
181
|
+
let cards = this.store.readCards(request.projectId).filter(live);
|
|
182
|
+
if (request.includeMastered !== true)
|
|
183
|
+
cards = cards.filter(card => card.status === 'active');
|
|
184
|
+
if (request.resourceId !== undefined)
|
|
185
|
+
cards = cards.filter(card => card.resourceId === request.resourceId);
|
|
186
|
+
if (request.nodeId !== undefined)
|
|
187
|
+
cards = cards.filter(card => card.contexts.some(context => context.nodeId === request.nodeId));
|
|
188
|
+
if (request.kind !== undefined)
|
|
189
|
+
cards = cards.filter(card => card.kind === request.kind);
|
|
190
|
+
if (request.source !== undefined)
|
|
191
|
+
cards = cards.filter(card => card.sources.some(source => source.type === request.source));
|
|
192
|
+
if (request.weakMin !== undefined)
|
|
193
|
+
cards = cards.filter(card => card.weakness >= request.weakMin);
|
|
194
|
+
if (request.due === true)
|
|
195
|
+
cards = cards.filter(card => card.mastery.dueAt <= now);
|
|
196
|
+
if (request.query !== undefined && request.query.trim() !== '')
|
|
197
|
+
cards = cards.filter(card => cardMatchesQuery(card, request.query));
|
|
198
|
+
const sort = request.sort ?? 'weak';
|
|
199
|
+
cards.sort((a, b) => sort === 'recent'
|
|
200
|
+
? b.updatedAt.localeCompare(a.updatedAt)
|
|
201
|
+
: sort === 'due'
|
|
202
|
+
? a.mastery.dueAt.localeCompare(b.mastery.dueAt)
|
|
203
|
+
: b.weakness - a.weakness || a.mastery.dueAt.localeCompare(b.mastery.dueAt));
|
|
204
|
+
return { cards: cards.slice(0, request.limit ?? 200), total: cards.length };
|
|
205
|
+
}
|
|
206
|
+
/** The cards due for review across projects (or one), weakest first. */
|
|
207
|
+
async dueCards(request) {
|
|
208
|
+
this.assertAlive('dueCards');
|
|
209
|
+
const now = new Date().toISOString();
|
|
210
|
+
const projectIds = request.projectId !== undefined ? [request.projectId] : this.store.readIndex().projects.map(project => project.id);
|
|
211
|
+
const due = [];
|
|
212
|
+
for (const projectId of projectIds) {
|
|
213
|
+
for (const card of this.store.readCards(projectId)) {
|
|
214
|
+
if (card.status === 'active' && card.mastery.dueAt <= now)
|
|
215
|
+
due.push({ ...card, projectId });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
due.sort((a, b) => a.mastery.box - b.mastery.box || b.weakness - a.weakness || a.mastery.dueAt.localeCompare(b.mastery.dueAt));
|
|
219
|
+
return { cards: due.slice(0, request.limit ?? 200) };
|
|
220
|
+
}
|
|
221
|
+
/** Edit a card by hand: its words, or its status (mastered / deleted / active). */
|
|
222
|
+
async editCard(request) {
|
|
223
|
+
this.assertAlive('editCard');
|
|
224
|
+
const project = this.readProjectOrRefusal(request.projectId);
|
|
225
|
+
if ('error' in project)
|
|
226
|
+
return project;
|
|
227
|
+
return await this.mutate(project.value.id, () => {
|
|
228
|
+
const cards = this.store.readCards(project.value.id);
|
|
229
|
+
const card = cards.find(candidate => candidate.id === request.cardId);
|
|
230
|
+
if (card === undefined)
|
|
231
|
+
return refuse('not-found', `no card ${JSON.stringify(request.cardId)}`);
|
|
232
|
+
const patch = request.patch;
|
|
233
|
+
if (patch.term !== undefined && patch.term.trim() === '')
|
|
234
|
+
return refuse('invalid', 'a card keeps a term');
|
|
235
|
+
if (patch.gloss !== undefined && patch.gloss.trim() === '')
|
|
236
|
+
return refuse('invalid', 'a card keeps a gloss');
|
|
237
|
+
const { resourceId: _old, ...rest } = card;
|
|
238
|
+
const next = {
|
|
239
|
+
...(patch.resourceId === null ? rest : card),
|
|
240
|
+
...(patch.term === undefined ? {} : { term: patch.term.trim() }),
|
|
241
|
+
...(patch.gloss === undefined ? {} : { gloss: patch.gloss.trim() }),
|
|
242
|
+
...(patch.userGloss === undefined ? {} : { userGloss: patch.userGloss.trim() }),
|
|
243
|
+
...(patch.aliases === undefined ? {} : { aliases: patch.aliases.map(alias => alias.trim()).filter(alias => alias !== '') }),
|
|
244
|
+
...(patch.status === undefined ? {} : { status: patch.status }),
|
|
245
|
+
...(typeof patch.resourceId === 'string' ? { resourceId: patch.resourceId } : {}),
|
|
246
|
+
updatedAt: new Date().toISOString(),
|
|
247
|
+
};
|
|
248
|
+
this.store.writeCards(project.value.id, cards.map(candidate => candidate.id === card.id ? next : candidate));
|
|
249
|
+
this.event(project.value.id, 'card.edited', { id: card.id, title: next.term });
|
|
250
|
+
return { ok: true, card: next };
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Undo one recording (the toast's 撤销): cards it created are deleted; cards
|
|
255
|
+
* it merged into lose the source it stacked (and the weakness it added).
|
|
256
|
+
*/
|
|
257
|
+
async undoCards(request) {
|
|
258
|
+
this.assertAlive('undoCards');
|
|
259
|
+
const project = this.readProjectOrRefusal(request.projectId);
|
|
260
|
+
if ('error' in project)
|
|
261
|
+
return project;
|
|
262
|
+
return await this.mutate(project.value.id, () => {
|
|
263
|
+
const wanted = new Set(request.ids);
|
|
264
|
+
let removed = 0;
|
|
265
|
+
let reverted = 0;
|
|
266
|
+
const cards = this.store.readCards(project.value.id).map(card => {
|
|
267
|
+
if (!wanted.has(card.id))
|
|
268
|
+
return card;
|
|
269
|
+
if (card.createdAt === request.at) {
|
|
270
|
+
removed += 1;
|
|
271
|
+
return { ...card, status: 'deleted', updatedAt: new Date().toISOString() };
|
|
272
|
+
}
|
|
273
|
+
const dropped = card.sources.filter(source => source.at === request.at);
|
|
274
|
+
if (dropped.length === 0)
|
|
275
|
+
return card;
|
|
276
|
+
reverted += 1;
|
|
277
|
+
return {
|
|
278
|
+
...card,
|
|
279
|
+
sources: card.sources.filter(source => source.at !== request.at),
|
|
280
|
+
weakness: clampWeakness(card.weakness - dropped.filter(source => source.weakened === true).length),
|
|
281
|
+
updatedAt: new Date().toISOString(),
|
|
282
|
+
};
|
|
283
|
+
});
|
|
284
|
+
this.store.writeCards(project.value.id, cards);
|
|
285
|
+
return { ok: true, removed, reverted };
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* One flashcard review (the daily 复习): only the schedule moves — forgot
|
|
290
|
+
* resets to box 0 (tomorrow), fuzzy keeps the box, recalled climbs one.
|
|
291
|
+
* Reviews never unlock nodes and never touch weakness.
|
|
292
|
+
*/
|
|
293
|
+
async reviewCard(request) {
|
|
294
|
+
this.assertAlive('reviewCard');
|
|
295
|
+
if (!['forgot', 'fuzzy', 'recalled'].includes(request.rating))
|
|
296
|
+
return refuse('invalid', 'rating is forgot | fuzzy | recalled');
|
|
297
|
+
const project = this.readProjectOrRefusal(request.projectId);
|
|
298
|
+
if ('error' in project)
|
|
299
|
+
return project;
|
|
300
|
+
return await this.mutate(project.value.id, () => {
|
|
301
|
+
const cards = this.store.readCards(project.value.id);
|
|
302
|
+
const card = cards.find(candidate => candidate.id === request.cardId && live(candidate));
|
|
303
|
+
if (card === undefined)
|
|
304
|
+
return refuse('not-found', `no card ${JSON.stringify(request.cardId)}`);
|
|
305
|
+
const now = new Date();
|
|
306
|
+
const box = request.rating === 'forgot' ? 0 : request.rating === 'fuzzy' ? card.mastery.box : Math.min(card.mastery.box + 1, MAX_BOX);
|
|
307
|
+
const next = {
|
|
308
|
+
...card,
|
|
309
|
+
mastery: {
|
|
310
|
+
box,
|
|
311
|
+
dueAt: leitnerDueAt(box, now),
|
|
312
|
+
streak: request.rating === 'recalled' ? card.mastery.streak + 1 : 0,
|
|
313
|
+
},
|
|
314
|
+
updatedAt: now.toISOString(),
|
|
315
|
+
};
|
|
316
|
+
this.store.writeCards(project.value.id, cards.map(candidate => candidate.id === card.id ? next : candidate));
|
|
317
|
+
return { ok: true, card: next };
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The library service core layer: the store, change listeners, disposal,
|
|
3
|
+
* the write boundary (`mutate`), the event ledger and the shared refusals.
|
|
4
|
+
*
|
|
5
|
+
* The service is ONE class split by domain into a chain of layers, each in
|
|
6
|
+
* its own file and each extending the one before:
|
|
7
|
+
*
|
|
8
|
+
* core → shelf (projects, resources) → cards → roadmap (paths, notes,
|
|
9
|
+
* 读完) → pages → assess (quizzes, attempts) → bindings (bindings, events,
|
|
10
|
+
* stats) = LibraryService
|
|
11
|
+
*
|
|
12
|
+
* A layer may call anything below it. The layering is organization only —
|
|
13
|
+
* `LibraryService` (service.ts re-exports it) is the one public class.
|
|
14
|
+
* Pure rules (shape validation, unlock, the evidence gate) live in
|
|
15
|
+
* `service/rules.ts`.
|
|
16
|
+
*
|
|
17
|
+
* @module @khorsheed/dsh-library/service/core
|
|
18
|
+
*/
|
|
19
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
20
|
+
import { type LibraryAttachmentRef, type LibraryCapabilities, type LibraryCard, type LibraryErrorCode, type LibraryEvent, type LibraryEventType, type LibraryOutcome, type LibraryProject, type LibraryRefusal } from '../types.ts';
|
|
21
|
+
import { LibraryStore } from '../store.ts';
|
|
22
|
+
export declare const PACKAGE_NAME = "@khorsheed/dsh-library";
|
|
23
|
+
/** The attachment store's faces, mirrored structurally (probed, never injected). */
|
|
24
|
+
export interface AttachmentsSlice {
|
|
25
|
+
readImage(ref: LibraryAttachmentRef): Promise<{
|
|
26
|
+
data: Uint8Array;
|
|
27
|
+
ref: {
|
|
28
|
+
mediaType: string;
|
|
29
|
+
};
|
|
30
|
+
}>;
|
|
31
|
+
saveImage?(input: {
|
|
32
|
+
data: Uint8Array;
|
|
33
|
+
mediaType: string;
|
|
34
|
+
}): Promise<LibraryAttachmentRef>;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Thrown by every public verb after {@link LibraryService.dispose} ran (an
|
|
38
|
+
* explicit teardown, or the borrowed Context's own disposal): a coded,
|
|
39
|
+
* package-named refusal, never a silent operation on a dead Context. The
|
|
40
|
+
* disposed instance stays dead — a rebuild over the same state root is a NEW
|
|
41
|
+
* service instance's business (the canvas-novel/sidechat precedent).
|
|
42
|
+
*/
|
|
43
|
+
export declare class LibraryDisposedError extends Error {
|
|
44
|
+
readonly code: 'disposed';
|
|
45
|
+
constructor(message: string, code: 'disposed');
|
|
46
|
+
}
|
|
47
|
+
/** A refusal in one line. */
|
|
48
|
+
export declare function refuse(error: LibraryErrorCode, message: string): LibraryRefusal;
|
|
49
|
+
/** Wrap a store failure as a refusal (the service's error channel is values). */
|
|
50
|
+
export declare function storeRefusal(error: unknown): LibraryRefusal;
|
|
51
|
+
/** The Leitner due date for a box: box b waits INTERVALS[min(b, 4)] days. */
|
|
52
|
+
export declare function leitnerDueAt(box: number, from?: Date): string;
|
|
53
|
+
/** Whitespace-normalize a quote before comparing it with a page's text. */
|
|
54
|
+
export declare function normalizeFragment(value: string): string;
|
|
55
|
+
/** Clamp a weakness score into 0..MAX. */
|
|
56
|
+
export declare function clampWeakness(value: number): number;
|
|
57
|
+
/** Live = not deleted. */
|
|
58
|
+
export declare function live(card: LibraryCard): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* The library's service core. Constructing it never throws and never touches
|
|
61
|
+
* the disk: the store probes writability lazily, so a read-only deployment
|
|
62
|
+
* boots and answers `storage-unavailable` on writes. Disposal follows the
|
|
63
|
+
* borrowed Context: an instance mounted dynamically (per state root, say)
|
|
64
|
+
* dies with its mounting Context, and a dead instance refuses every verb
|
|
65
|
+
* coded, reads included.
|
|
66
|
+
*/
|
|
67
|
+
export declare class LibraryServiceCore {
|
|
68
|
+
protected readonly ctx: Context;
|
|
69
|
+
/** The store; public for the package's own tests and diagnostics. */
|
|
70
|
+
readonly store: LibraryStore;
|
|
71
|
+
/** Listeners told after every project mutation (the tab's live refresh, the context cache). */
|
|
72
|
+
protected readonly listeners: Set<(projectId: string) => void>;
|
|
73
|
+
/** When this instance last wrote anything (a project created, an index edit — writes that log no event). */
|
|
74
|
+
protected lastChangeAt: string | undefined;
|
|
75
|
+
/** The newest session-made write (a library tool from a session): the client's cue to reveal the sidebar. */
|
|
76
|
+
protected lastSessionWrite: {
|
|
77
|
+
readonly session: string;
|
|
78
|
+
readonly at: string;
|
|
79
|
+
} | undefined;
|
|
80
|
+
/** Set by {@link dispose} (explicit, or the borrowed Context's teardown); every public verb refuses afterwards. */
|
|
81
|
+
protected disposed: boolean;
|
|
82
|
+
constructor(ctx: Context, config?: {
|
|
83
|
+
stateRoot?: string;
|
|
84
|
+
});
|
|
85
|
+
/**
|
|
86
|
+
* Refuse one public verb after disposal: the instance is dead and nothing
|
|
87
|
+
* it could still answer is meant to be consumed — reads included, so a
|
|
88
|
+
* mounting side never keeps consuming a stale instance indefinitely.
|
|
89
|
+
*/
|
|
90
|
+
protected assertAlive(verb: string): void;
|
|
91
|
+
/**
|
|
92
|
+
* Tear the instance down (also wired to the borrowed Context's disposal at
|
|
93
|
+
* construction). Idempotent and TERMINAL: the listener set is dropped and
|
|
94
|
+
* afterwards every public verb refuses with {@link LibraryDisposedError};
|
|
95
|
+
* a write that already passed the write boundary may complete (the store's
|
|
96
|
+
* lock/temp-write/atomic-rename keeps the disk side safe), one reaching it
|
|
97
|
+
* after disposal refuses before lock acquisition. A rebuild over the same
|
|
98
|
+
* state root is a NEW instance's business.
|
|
99
|
+
*/
|
|
100
|
+
dispose(): void;
|
|
101
|
+
/**
|
|
102
|
+
* What a client may rely on in this composition: storage availability with
|
|
103
|
+
* its reason, and the attachment probe's verdict at call time. A read-only
|
|
104
|
+
* display contract — enforcement stays at the write gates, unchanged.
|
|
105
|
+
*/
|
|
106
|
+
capabilities(): Promise<LibraryCapabilities>;
|
|
107
|
+
/** Subscribe to project mutations; returns the unsubscribe. */
|
|
108
|
+
onChange(listener: (projectId: string) => void): () => void;
|
|
109
|
+
protected changed(projectId: string): void;
|
|
110
|
+
/** Run a project mutation under its lock, then notify. */
|
|
111
|
+
protected mutate<T>(projectId: string, fn: () => Promise<LibraryOutcome<T>> | LibraryOutcome<T>): Promise<LibraryOutcome<T>>;
|
|
112
|
+
/** A library tool from this session just wrote (kit.autoBind calls it after every successful write). */
|
|
113
|
+
noteSessionWrite(session: string): void;
|
|
114
|
+
protected event(projectId: string, type: LibraryEventType, fields?: Omit<LibraryEvent, 'type' | 'at'> & {
|
|
115
|
+
at?: string;
|
|
116
|
+
}): void;
|
|
117
|
+
/** The project, or a not-found refusal that teaches where the list lives. */
|
|
118
|
+
protected readProjectOrRefusal(projectId: string): {
|
|
119
|
+
value: LibraryProject;
|
|
120
|
+
} | (LibraryRefusal & {
|
|
121
|
+
error: LibraryErrorCode;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=core.d.ts.map
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { LEITNER_INTERVAL_DAYS, libraryErrorMessage, MAX_WEAKNESS, } from "../types.js";
|
|
2
|
+
import { LibraryStore, LibraryStoreError } from "../store.js";
|
|
3
|
+
export const PACKAGE_NAME = '@khorsheed/dsh-library';
|
|
4
|
+
/**
|
|
5
|
+
* Thrown by every public verb after {@link LibraryService.dispose} ran (an
|
|
6
|
+
* explicit teardown, or the borrowed Context's own disposal): a coded,
|
|
7
|
+
* package-named refusal, never a silent operation on a dead Context. The
|
|
8
|
+
* disposed instance stays dead — a rebuild over the same state root is a NEW
|
|
9
|
+
* service instance's business (the canvas-novel/sidechat precedent).
|
|
10
|
+
*/
|
|
11
|
+
export class LibraryDisposedError extends Error {
|
|
12
|
+
code;
|
|
13
|
+
constructor(message, code) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.name = 'LibraryDisposedError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** A refusal in one line. */
|
|
20
|
+
export function refuse(error, message) {
|
|
21
|
+
return { ok: false, error, message };
|
|
22
|
+
}
|
|
23
|
+
/** Wrap a store failure as a refusal (the service's error channel is values). */
|
|
24
|
+
export function storeRefusal(error) {
|
|
25
|
+
if (error instanceof LibraryStoreError) {
|
|
26
|
+
const code = error.kind === 'corrupt' ? 'corrupt' : error.kind === 'unavailable' ? 'storage-unavailable' : 'io';
|
|
27
|
+
return refuse(code, error.message);
|
|
28
|
+
}
|
|
29
|
+
return refuse('io', `library: ${libraryErrorMessage(error)}`);
|
|
30
|
+
}
|
|
31
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
32
|
+
/** The Leitner due date for a box: box b waits INTERVALS[min(b, 4)] days. */
|
|
33
|
+
export function leitnerDueAt(box, from = new Date()) {
|
|
34
|
+
const days = LEITNER_INTERVAL_DAYS[Math.min(Math.max(box, 0), LEITNER_INTERVAL_DAYS.length - 1)];
|
|
35
|
+
return new Date(from.getTime() + days * DAY_MS).toISOString();
|
|
36
|
+
}
|
|
37
|
+
/** Whitespace-normalize a quote before comparing it with a page's text. */
|
|
38
|
+
export function normalizeFragment(value) {
|
|
39
|
+
return value.normalize('NFKC').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
40
|
+
}
|
|
41
|
+
/** Clamp a weakness score into 0..MAX. */
|
|
42
|
+
export function clampWeakness(value) {
|
|
43
|
+
return Math.min(Math.max(value, 0), MAX_WEAKNESS);
|
|
44
|
+
}
|
|
45
|
+
/** Live = not deleted. */
|
|
46
|
+
export function live(card) {
|
|
47
|
+
return card.status !== 'deleted';
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The library's service core. Constructing it never throws and never touches
|
|
51
|
+
* the disk: the store probes writability lazily, so a read-only deployment
|
|
52
|
+
* boots and answers `storage-unavailable` on writes. Disposal follows the
|
|
53
|
+
* borrowed Context: an instance mounted dynamically (per state root, say)
|
|
54
|
+
* dies with its mounting Context, and a dead instance refuses every verb
|
|
55
|
+
* coded, reads included.
|
|
56
|
+
*/
|
|
57
|
+
export class LibraryServiceCore {
|
|
58
|
+
ctx;
|
|
59
|
+
/** The store; public for the package's own tests and diagnostics. */
|
|
60
|
+
store;
|
|
61
|
+
/** Listeners told after every project mutation (the tab's live refresh, the context cache). */
|
|
62
|
+
listeners = new Set();
|
|
63
|
+
/** When this instance last wrote anything (a project created, an index edit — writes that log no event). */
|
|
64
|
+
lastChangeAt;
|
|
65
|
+
/** The newest session-made write (a library tool from a session): the client's cue to reveal the sidebar. */
|
|
66
|
+
lastSessionWrite;
|
|
67
|
+
/** Set by {@link dispose} (explicit, or the borrowed Context's teardown); every public verb refuses afterwards. */
|
|
68
|
+
disposed = false;
|
|
69
|
+
constructor(ctx, config = {}) {
|
|
70
|
+
this.ctx = ctx;
|
|
71
|
+
this.store = new LibraryStore(config);
|
|
72
|
+
// The borrowed Context's disposal IS this instance's disposal: an
|
|
73
|
+
// instance mounted per state root dies with its mounting Context instead
|
|
74
|
+
// of answering verbs off a dead scope.
|
|
75
|
+
ctx.effect(() => () => { this.dispose(); }, 'library: dispose on context teardown');
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Refuse one public verb after disposal: the instance is dead and nothing
|
|
79
|
+
* it could still answer is meant to be consumed — reads included, so a
|
|
80
|
+
* mounting side never keeps consuming a stale instance indefinitely.
|
|
81
|
+
*/
|
|
82
|
+
assertAlive(verb) {
|
|
83
|
+
if (this.disposed)
|
|
84
|
+
throw new LibraryDisposedError(`${PACKAGE_NAME}: ${verb} after dispose`, 'disposed');
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Tear the instance down (also wired to the borrowed Context's disposal at
|
|
88
|
+
* construction). Idempotent and TERMINAL: the listener set is dropped and
|
|
89
|
+
* afterwards every public verb refuses with {@link LibraryDisposedError};
|
|
90
|
+
* a write that already passed the write boundary may complete (the store's
|
|
91
|
+
* lock/temp-write/atomic-rename keeps the disk side safe), one reaching it
|
|
92
|
+
* after disposal refuses before lock acquisition. A rebuild over the same
|
|
93
|
+
* state root is a NEW instance's business.
|
|
94
|
+
*/
|
|
95
|
+
dispose() {
|
|
96
|
+
this.disposed = true;
|
|
97
|
+
this.listeners.clear();
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* What a client may rely on in this composition: storage availability with
|
|
101
|
+
* its reason, and the attachment probe's verdict at call time. A read-only
|
|
102
|
+
* display contract — enforcement stays at the write gates, unchanged.
|
|
103
|
+
*/
|
|
104
|
+
async capabilities() {
|
|
105
|
+
this.assertAlive('capabilities');
|
|
106
|
+
return {
|
|
107
|
+
protocolVersion: 1,
|
|
108
|
+
hasStorage: this.store.available,
|
|
109
|
+
...(this.store.unavailableReason === undefined ? {} : { storageUnavailableReason: this.store.unavailableReason }),
|
|
110
|
+
hasAttachments: this.ctx.get('attachments') !== undefined,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/** Subscribe to project mutations; returns the unsubscribe. */
|
|
114
|
+
onChange(listener) {
|
|
115
|
+
this.assertAlive('onChange');
|
|
116
|
+
this.listeners.add(listener);
|
|
117
|
+
return () => { this.listeners.delete(listener); };
|
|
118
|
+
}
|
|
119
|
+
changed(projectId) {
|
|
120
|
+
this.lastChangeAt = new Date().toISOString();
|
|
121
|
+
for (const listener of this.listeners) {
|
|
122
|
+
try {
|
|
123
|
+
listener(projectId);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// A listener's failure is its own.
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** Run a project mutation under its lock, then notify. */
|
|
131
|
+
async mutate(projectId, fn) {
|
|
132
|
+
// The write boundary: a disposal that landed since the entry guard dies
|
|
133
|
+
// here, BEFORE the lock is taken — never queuing onto a lock a rebuilt
|
|
134
|
+
// instance on the same root now owns (kept outside the try, so the coded
|
|
135
|
+
// refusal propagates as itself rather than folding into a store refusal).
|
|
136
|
+
this.assertAlive('mutate');
|
|
137
|
+
try {
|
|
138
|
+
const outcome = await this.store.withProjectLock(projectId, fn);
|
|
139
|
+
if (outcome.ok)
|
|
140
|
+
this.changed(projectId);
|
|
141
|
+
return outcome;
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
return storeRefusal(error);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** A library tool from this session just wrote (kit.autoBind calls it after every successful write). */
|
|
148
|
+
noteSessionWrite(session) {
|
|
149
|
+
this.lastSessionWrite = { session, at: new Date().toISOString() };
|
|
150
|
+
}
|
|
151
|
+
event(projectId, type, fields = {}) {
|
|
152
|
+
this.store.appendEvents(projectId, [{ at: fields.at ?? new Date().toISOString(), type, ...fields }]);
|
|
153
|
+
}
|
|
154
|
+
/* -------------------------------------------------------------- helpers */
|
|
155
|
+
/** The project, or a not-found refusal that teaches where the list lives. */
|
|
156
|
+
readProjectOrRefusal(projectId) {
|
|
157
|
+
let project;
|
|
158
|
+
try {
|
|
159
|
+
project = this.store.readProject(projectId);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
return storeRefusal(error);
|
|
163
|
+
}
|
|
164
|
+
if (project === undefined) {
|
|
165
|
+
return refuse('not-found', `no project ${JSON.stringify(projectId)} — library_read · projects lists every project; create one with library_project`);
|
|
166
|
+
}
|
|
167
|
+
return { value: project };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Photographed pages: recording (marks carded), listing, photo reads and saves.
|
|
3
|
+
*
|
|
4
|
+
* One layer of the library service (see `service/core.ts` for the layering):
|
|
5
|
+
* the methods are the service's own, moved here verbatim by domain.
|
|
6
|
+
*
|
|
7
|
+
* @module @khorsheed/dsh-library/service/pages
|
|
8
|
+
*/
|
|
9
|
+
import { type LibraryAttachmentRef, type LibraryCardWriteResult, type LibraryMark, type LibraryOutcome, type LibraryPage } from '../types.ts';
|
|
10
|
+
import { LibraryServiceRoadmap } from './roadmap.ts';
|
|
11
|
+
export declare class LibraryServicePages extends LibraryServiceRoadmap {
|
|
12
|
+
/**
|
|
13
|
+
* Record one photographed page and card every mark at once (「一键全收」).
|
|
14
|
+
* Uncertain marks are kept on the page but never carded — the learner
|
|
15
|
+
* decides them. Each card's gloss is the mark's recognized gloss, else the
|
|
16
|
+
* learner's handwriting, else a placeholder the learner can fill in.
|
|
17
|
+
*/
|
|
18
|
+
recordPage(request: {
|
|
19
|
+
projectId: string;
|
|
20
|
+
resourceId: string;
|
|
21
|
+
nodeId?: string;
|
|
22
|
+
label?: string;
|
|
23
|
+
attachment: LibraryAttachmentRef;
|
|
24
|
+
text: string;
|
|
25
|
+
marks: readonly LibraryMark[];
|
|
26
|
+
}): Promise<LibraryOutcome<{
|
|
27
|
+
page: LibraryPage;
|
|
28
|
+
cards: LibraryCardWriteResult;
|
|
29
|
+
uncertain: string[];
|
|
30
|
+
}>>;
|
|
31
|
+
/** One resource's pages, optionally one node's. */
|
|
32
|
+
listPages(request: {
|
|
33
|
+
projectId: string;
|
|
34
|
+
resourceId: string;
|
|
35
|
+
nodeId?: string;
|
|
36
|
+
}): Promise<{
|
|
37
|
+
pages: LibraryPage[];
|
|
38
|
+
}>;
|
|
39
|
+
/** Find one page anywhere in the project. */
|
|
40
|
+
protected findPage(projectId: string, pageId: string): LibraryPage | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* One page's photo, as base64 — whitelisted: the page must be one this
|
|
43
|
+
* project recorded, so the Remote never reads an arbitrary attachment.
|
|
44
|
+
*/
|
|
45
|
+
readPagePhoto(request: {
|
|
46
|
+
projectId: string;
|
|
47
|
+
pageId: string;
|
|
48
|
+
}): Promise<LibraryOutcome<{
|
|
49
|
+
data: string;
|
|
50
|
+
mediaType: string;
|
|
51
|
+
}>>;
|
|
52
|
+
/** Save a browser-uploaded photo into the host attachment store (the sidebar's 拍照). */
|
|
53
|
+
savePhoto(request: {
|
|
54
|
+
data: string;
|
|
55
|
+
mediaType: string;
|
|
56
|
+
}): Promise<LibraryOutcome<{
|
|
57
|
+
attachment: LibraryAttachmentRef;
|
|
58
|
+
}>>;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=pages.d.ts.map
|