dsh-multimodal-node 0.0.0-stage → 0.2.2

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.
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Saved prompts: a small per-account library of prompts a person wants to reuse.
3
+ *
4
+ * Why a separate file and not a workflow: a saved prompt is not a graph. It is the text a
5
+ * person typed (plus the negatives), reusable across workflows — the whole point of
6
+ * favouriting "白衬衫, 雨夜, 便利店" is using it on more than the graph that produced it.
7
+ *
8
+ * Storage shape is `{ byOwner: { <user>: [ {id, name, positive, negative, layers[], at} ] } }`,
9
+ * written atomically the same way the run index is, so a crash mid-save cannot truncate
10
+ * someone else's list.
11
+ */
12
+ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
13
+ import { join } from 'node:path';
14
+ import { randomBytes } from 'node:crypto';
15
+
16
+ const MAX_PROMPTS = 200;
17
+
18
+ export class PromptLibrary {
19
+ constructor(dataDir) {
20
+ this.file = join(dataDir, 'prompts.json');
21
+ this.byOwner = {};
22
+ }
23
+
24
+ async init() {
25
+ await mkdir(join(this.file, '..'), { recursive: true });
26
+ try {
27
+ const raw = await readFile(this.file, 'utf8');
28
+ const parsed = JSON.parse(raw);
29
+ this.byOwner = parsed !== null && typeof parsed.byOwner === 'object' && parsed.byOwner !== null ? parsed.byOwner : {};
30
+ } catch {
31
+ this.byOwner = {};
32
+ await this.save();
33
+ }
34
+ }
35
+
36
+ async save() {
37
+ const temp = `${this.file}.tmp`;
38
+ await writeFile(temp, `${JSON.stringify({ byOwner: this.byOwner }, null, 2)}\n`, 'utf8');
39
+ await rename(temp, this.file);
40
+ }
41
+
42
+ /** One account's prompts, newest first. */
43
+ list(owner) {
44
+ const rows = Array.isArray(this.byOwner[owner]) ? this.byOwner[owner] : [];
45
+ return rows.slice().sort((left, right) => (right.at ?? 0) - (left.at ?? 0));
46
+ }
47
+
48
+ /**
49
+ * Save one prompt. Saving the same name twice updates that entry instead of piling up
50
+ * duplicates — a person who fixes a typo in a favourite does not want two of them.
51
+ */
52
+ async add(owner, input) {
53
+ const name = String(input?.name ?? '').trim().slice(0, 60);
54
+ if (name === '') throw new Error('收藏需要一个名字');
55
+ const positive = String(input?.positive ?? '').trim();
56
+ const negative = String(input?.negative ?? '').trim();
57
+ const layers = Array.isArray(input?.layers) ? input.layers.map((v) => String(v ?? '')) : [];
58
+ if (positive === '' && negative === '' && layers.every((v) => v === '')) {
59
+ throw new Error('提示词是空的,没有可收藏的内容');
60
+ }
61
+ const rows = Array.isArray(this.byOwner[owner]) ? this.byOwner[owner] : [];
62
+ const existing = rows.find((row) => row.name === name);
63
+ const record = {
64
+ id: existing?.id ?? `p-${randomBytes(4).toString('hex')}`,
65
+ name,
66
+ positive,
67
+ negative,
68
+ layers,
69
+ at: Date.now(),
70
+ };
71
+ if (existing === undefined) {
72
+ rows.unshift(record);
73
+ // Newest-first order is maintained on read, but cap the stored list so a
74
+ // lifetime of favourites cannot grow the file without bound.
75
+ this.byOwner[owner] = rows.slice(0, MAX_PROMPTS);
76
+ } else {
77
+ rows[rows.indexOf(existing)] = record;
78
+ this.byOwner[owner] = rows;
79
+ }
80
+ await this.save();
81
+ return record;
82
+ }
83
+
84
+ async remove(owner, id) {
85
+ const rows = Array.isArray(this.byOwner[owner]) ? this.byOwner[owner] : [];
86
+ const next = rows.filter((row) => row.id !== id);
87
+ if (next.length === rows.length) throw new Error('没有这条收藏');
88
+ this.byOwner[owner] = next;
89
+ await this.save();
90
+ return { ok: true };
91
+ }
92
+ }