@huanlin/dsh-plugin-aigc-canvas 0.1.10 → 0.2.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/cordis.patch.yml +2 -2
- package/lib/client.js +276 -153
- package/lib/client.js.map +1 -1
- package/lib/config.d.ts +49 -11
- package/lib/context-types.d.ts +26 -0
- package/lib/entry-identity.d.ts +22 -0
- package/lib/index.d.ts +6 -4
- package/lib/index.js +291 -202
- package/lib/provider-shape.d.ts +15 -0
- package/lib/provider-store.d.ts +57 -22
- package/lib/settings.d.ts +78 -0
- package/lib/tools.d.ts +3 -3
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +18 -10
package/lib/index.js
CHANGED
|
@@ -1,24 +1,45 @@
|
|
|
1
|
-
import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { basename, dirname, isAbsolute, join, sep } from "node:path";
|
|
4
4
|
import { WebSocket, WebSocketServer } from "ws";
|
|
5
5
|
import z from "@deepseek-ai/schemastery";
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
7
|
import { homedir } from "node:os";
|
|
7
|
-
import { readFileSync } from "node:fs";
|
|
8
8
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { spawn } from "node:child_process";
|
|
11
|
+
//#region src/provider-shape.ts
|
|
12
|
+
/**
|
|
13
|
+
* Pure provider-id validation shared by the host half (Config / ProviderStore)
|
|
14
|
+
* and the client settings page. Kept free of schemastery imports so the
|
|
15
|
+
* client bundle (tsdown purity gate) can inline it.
|
|
16
|
+
*
|
|
17
|
+
* @module @huanlin/dsh-plugin-aigc-canvas/provider-shape
|
|
18
|
+
*/
|
|
19
|
+
/** Provider id pattern: lowercase letters, digits, hyphens; must start with a letter. */
|
|
20
|
+
const PROVIDER_ID_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
21
|
+
/**
|
|
22
|
+
* Validate a provider id.
|
|
23
|
+
* @param id - candidate id (as typed into the settings page or passed to the store).
|
|
24
|
+
* @returns an error message, or undefined when valid.
|
|
25
|
+
*/
|
|
26
|
+
function validateProviderId(id) {
|
|
27
|
+
if (id === "") return "provider id is required";
|
|
28
|
+
if (!PROVIDER_ID_PATTERN.test(id)) return `invalid provider id: ${JSON.stringify(id)} (must be lowercase, hyphenated, start with a letter)`;
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
11
31
|
//#region src/config.ts
|
|
12
32
|
/**
|
|
13
33
|
* Serializable configuration and defaults for the AIGC canvas host half.
|
|
14
34
|
* The `providers` array holds one or more AIGC provider configs (name /
|
|
15
|
-
* endpoint / apiKey / instructions)
|
|
16
|
-
*
|
|
35
|
+
* endpoint / apiKey / instructions). Since dsh 0.1.7-rc.1 the editable
|
|
36
|
+
* fields are `.volatile()` members of the entry's profile-owned Cordis
|
|
37
|
+
* Config (DSH-0.1.7-J1-04): the composition `config:` in cordis.patch.yml is
|
|
38
|
+
* the first-boot seed only, and runtime edits persist per profile under the
|
|
39
|
+
* entry id `dsh-aigc-canvas` through the settings service.
|
|
17
40
|
*
|
|
18
41
|
* @module @huanlin/dsh-plugin-aigc-canvas/config
|
|
19
42
|
*/
|
|
20
|
-
/** Provider id pattern: lowercase letters, digits, hyphens; must start with a letter. */
|
|
21
|
-
const PROVIDER_ID_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
22
43
|
/** Schemastery schema for the per-provider auth config. */
|
|
23
44
|
const ProviderAuthSchema = z.object({
|
|
24
45
|
scheme: z.union([
|
|
@@ -41,7 +62,14 @@ const ProviderSchema = z.object({
|
|
|
41
62
|
}),
|
|
42
63
|
builtin: z.boolean().description("Whether this provider is a builtin seed (cordis.yml).").default(false)
|
|
43
64
|
});
|
|
44
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* Schemastery schema for the plugin's profile-owned Config.
|
|
67
|
+
*
|
|
68
|
+
* `providers` is `.volatile()` (dsh 0.1.7-rc.1 DSH-0.1.7-J1-04): the settings
|
|
69
|
+
* service enumerates the entry's volatile fields for the configuration form,
|
|
70
|
+
* and a committed edit updates the running reference in place. The numeric
|
|
71
|
+
* knobs stay non-volatile (cordis.patch.yml seed only).
|
|
72
|
+
*/
|
|
45
73
|
const Config = z.object({
|
|
46
74
|
providers: z.array(ProviderSchema).description("One or more AIGC providers; the first is the default.").default([{
|
|
47
75
|
id: "stub",
|
|
@@ -54,7 +82,7 @@ const Config = z.object({
|
|
|
54
82
|
name: ""
|
|
55
83
|
},
|
|
56
84
|
builtin: true
|
|
57
|
-
}]),
|
|
85
|
+
}]).volatile(),
|
|
58
86
|
requestTimeoutMs: z.number().step(1).min(1e3).default(3e5),
|
|
59
87
|
mediaSizeLimit: z.number().step(1).min(1024).default(104857600)
|
|
60
88
|
});
|
|
@@ -62,11 +90,19 @@ const Config = z.object({
|
|
|
62
90
|
function isStubEndpoint(endpoint) {
|
|
63
91
|
return endpoint === "" || endpoint === "stub://aigc-backend";
|
|
64
92
|
}
|
|
65
|
-
/**
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
93
|
+
/** The fallback stub provider seeded when no provider is configured at load. */
|
|
94
|
+
const DEFAULT_STUB_PROVIDER = {
|
|
95
|
+
id: "stub",
|
|
96
|
+
name: "",
|
|
97
|
+
endpoint: "stub://aigc-backend",
|
|
98
|
+
apiKey: "",
|
|
99
|
+
instructions: "",
|
|
100
|
+
auth: {
|
|
101
|
+
scheme: "bearer",
|
|
102
|
+
name: ""
|
|
103
|
+
},
|
|
104
|
+
builtin: true
|
|
105
|
+
};
|
|
70
106
|
/** Migrate + resolve a single provider from config input. */
|
|
71
107
|
function resolveProvider(p) {
|
|
72
108
|
const auth = p.auth ?? {};
|
|
@@ -83,21 +119,35 @@ function resolveProvider(p) {
|
|
|
83
119
|
builtin: p.builtin ?? false
|
|
84
120
|
};
|
|
85
121
|
}
|
|
86
|
-
/**
|
|
122
|
+
/**
|
|
123
|
+
* Normalize a raw provider list (the volatile reference's latest snapshot, a
|
|
124
|
+
* legacy imported document, or a hand-edited override) into resolved
|
|
125
|
+
* providers. Non-object entries are skipped; duplicate ids keep the first
|
|
126
|
+
* occurrence (insertion order preserved).
|
|
127
|
+
* @param raw - the raw list value.
|
|
128
|
+
* @returns the resolved providers, in order, deduplicated by id.
|
|
129
|
+
*/
|
|
130
|
+
function resolveAigcProviders(raw) {
|
|
131
|
+
if (!Array.isArray(raw)) return [];
|
|
132
|
+
const byId = /* @__PURE__ */ new Map();
|
|
133
|
+
for (const item of raw) {
|
|
134
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) continue;
|
|
135
|
+
const resolved = resolveProvider(item);
|
|
136
|
+
if (!byId.has(resolved.id)) byId.set(resolved.id, resolved);
|
|
137
|
+
}
|
|
138
|
+
return [...byId.values()];
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Resolve the apply-time seed config (direct-call defaults after Loader
|
|
142
|
+
* schema validation has normally run). The provider list reads the live
|
|
143
|
+
* volatile reference; when it resolves empty at load, the default stub is
|
|
144
|
+
* seeded so the tools always have one provider.
|
|
145
|
+
* @param config - the entry config the Loader passed to `apply`.
|
|
146
|
+
* @returns the fully defaulted seed settings.
|
|
147
|
+
*/
|
|
87
148
|
function resolveAigcConfig(config) {
|
|
88
|
-
const providers = (config?.providers
|
|
89
|
-
if (providers.length === 0) providers.push(
|
|
90
|
-
id: "stub",
|
|
91
|
-
name: "",
|
|
92
|
-
endpoint: "stub://aigc-backend",
|
|
93
|
-
apiKey: "",
|
|
94
|
-
instructions: "",
|
|
95
|
-
auth: {
|
|
96
|
-
scheme: "bearer",
|
|
97
|
-
name: ""
|
|
98
|
-
},
|
|
99
|
-
builtin: true
|
|
100
|
-
});
|
|
149
|
+
const providers = [...resolveAigcProviders(config?.providers?.get())];
|
|
150
|
+
if (providers.length === 0) providers.push(DEFAULT_STUB_PROVIDER);
|
|
101
151
|
return {
|
|
102
152
|
providers,
|
|
103
153
|
requestTimeoutMs: config?.requestTimeoutMs ?? 3e5,
|
|
@@ -107,184 +157,259 @@ function resolveAigcConfig(config) {
|
|
|
107
157
|
//#endregion
|
|
108
158
|
//#region src/provider-store.ts
|
|
109
159
|
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
* page RPC share one instance per plugin fiber. Persisted to
|
|
113
|
-
* `~/.dsh/aigc-canvas/providers.json` so restarts keep user-added
|
|
114
|
-
* providers and instructions.
|
|
160
|
+
* Mutable provider store. Owns the canonical provider list; tool
|
|
161
|
+
* registration shares one instance per plugin fiber.
|
|
115
162
|
*
|
|
116
|
-
*
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const PROVIDERS_JSON = join(DATA_DIR, "providers.json");
|
|
122
|
-
/** Atomic write: mkdir + temp file + rename. */
|
|
123
|
-
async function writeJsonAtomic$1(path, value) {
|
|
124
|
-
const tmp = `${path}.tmp-${process.pid}`;
|
|
125
|
-
try {
|
|
126
|
-
await mkdir(dirname(path), { recursive: true });
|
|
127
|
-
await writeFile(tmp, JSON.stringify(value, null, 2), "utf8");
|
|
128
|
-
await rename(tmp, path);
|
|
129
|
-
} catch {}
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Mutable provider store. Owns the canonical provider list; the backend
|
|
133
|
-
* client map and RPC handlers share one instance per plugin fiber.
|
|
134
|
-
*
|
|
135
|
-
* Persistence: on construction the store loads `~/.dsh/aigc-canvas/
|
|
136
|
-
* providers.json` (if present) and merges it over the cordis.yml seed —
|
|
137
|
-
* persisted providers win, so user edits and deletions survive restarts.
|
|
138
|
-
* Every mutation writes the list back to disk (fire-and-forget).
|
|
163
|
+
* While no persistence face is attached the in-memory map is canonical
|
|
164
|
+
* (headless mode). Once a face is attached, every read resolves through the
|
|
165
|
+
* face's source and every mutation commits through `persist` before its
|
|
166
|
+
* result is reported — the committed profile config is the single source of
|
|
167
|
+
* truth.
|
|
139
168
|
*/
|
|
140
169
|
var ProviderStore = class {
|
|
141
170
|
providers = /* @__PURE__ */ new Map();
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
constructor(seed
|
|
146
|
-
this.
|
|
147
|
-
const seedBuiltin = new Map(seed.map((p) => [p.id, p.builtin ?? false]));
|
|
148
|
-
const sources = loadPersistedSync(dataPath) ?? seed;
|
|
149
|
-
for (const p of sources) {
|
|
150
|
-
const resolved = {
|
|
151
|
-
id: p.id,
|
|
152
|
-
name: p.name ?? "",
|
|
153
|
-
endpoint: p.endpoint ?? "stub://aigc-backend",
|
|
154
|
-
apiKey: p.apiKey ?? "",
|
|
155
|
-
instructions: p.instructions ?? "",
|
|
156
|
-
auth: {
|
|
157
|
-
scheme: p.auth?.scheme ?? "bearer",
|
|
158
|
-
name: p.auth?.name ?? ""
|
|
159
|
-
},
|
|
160
|
-
builtin: seedBuiltin.get(p.id) ?? false
|
|
161
|
-
};
|
|
162
|
-
this.providers.set(resolved.id, resolved);
|
|
163
|
-
}
|
|
171
|
+
/** Optional persistence face; absent in headless mode. */
|
|
172
|
+
persistence;
|
|
173
|
+
/** @param seed - resolved seed providers (the composition layer). */
|
|
174
|
+
constructor(seed) {
|
|
175
|
+
for (const provider of seed) this.providers.set(provider.id, provider);
|
|
164
176
|
}
|
|
165
|
-
/**
|
|
177
|
+
/**
|
|
178
|
+
* Attach a persistence face. Subsequent reads derive from the face's
|
|
179
|
+
* source and mutations commit through it.
|
|
180
|
+
* @param persistence - the read/write face over the entry's config.
|
|
181
|
+
*/
|
|
182
|
+
attachPersistence(persistence) {
|
|
183
|
+
this.persistence = persistence;
|
|
184
|
+
}
|
|
185
|
+
/** The committed provider list (insertion order; duplicates keep the first). */
|
|
186
|
+
committed() {
|
|
187
|
+
if (this.persistence === void 0) return [...this.providers.values()];
|
|
188
|
+
return resolveAigcProviders(this.persistence.source());
|
|
189
|
+
}
|
|
190
|
+
/** Snapshot of all providers, in order. */
|
|
166
191
|
list() {
|
|
167
|
-
return
|
|
192
|
+
return this.committed();
|
|
168
193
|
}
|
|
169
194
|
/** Look up one provider by id. */
|
|
170
195
|
get(id) {
|
|
171
|
-
return this.
|
|
196
|
+
return this.committed().find((provider) => provider.id === id);
|
|
172
197
|
}
|
|
173
|
-
/** The default provider (first in
|
|
198
|
+
/** The default provider (first in order); undefined if empty. */
|
|
174
199
|
defaultProvider() {
|
|
175
|
-
return this.
|
|
200
|
+
return this.committed()[0];
|
|
176
201
|
}
|
|
177
202
|
/** Add a new provider. Returns failure for duplicate id or invalid shape. */
|
|
178
|
-
add(provider) {
|
|
203
|
+
async add(provider) {
|
|
179
204
|
const idError = validateProviderId(provider.id);
|
|
180
205
|
if (idError !== void 0) return {
|
|
181
206
|
ok: false,
|
|
182
207
|
error: idError
|
|
183
208
|
};
|
|
184
|
-
|
|
209
|
+
const committed = this.committed();
|
|
210
|
+
if (committed.some((p) => p.id === provider.id)) return {
|
|
185
211
|
ok: false,
|
|
186
212
|
error: `provider id already exists: ${provider.id}`
|
|
187
213
|
};
|
|
188
214
|
const stored = {
|
|
189
|
-
|
|
190
|
-
name: provider.name ?? "",
|
|
191
|
-
endpoint: provider.endpoint ?? "stub://aigc-backend",
|
|
192
|
-
apiKey: provider.apiKey ?? "",
|
|
193
|
-
instructions: provider.instructions ?? "",
|
|
194
|
-
auth: {
|
|
195
|
-
scheme: provider.auth?.scheme ?? "bearer",
|
|
196
|
-
name: provider.auth?.name ?? ""
|
|
197
|
-
},
|
|
215
|
+
...resolveStored(provider),
|
|
198
216
|
builtin: false
|
|
199
217
|
};
|
|
200
|
-
this.
|
|
201
|
-
this.persist();
|
|
202
|
-
return {
|
|
203
|
-
ok: true,
|
|
204
|
-
providers: this.list()
|
|
205
|
-
};
|
|
218
|
+
return this.commit([...committed, stored]);
|
|
206
219
|
}
|
|
207
220
|
/** Update an existing provider. Returns failure if the id is unknown. */
|
|
208
|
-
update(provider) {
|
|
221
|
+
async update(provider) {
|
|
209
222
|
const idError = validateProviderId(provider.id);
|
|
210
223
|
if (idError !== void 0) return {
|
|
211
224
|
ok: false,
|
|
212
225
|
error: idError
|
|
213
226
|
};
|
|
214
|
-
const
|
|
227
|
+
const committed = this.committed();
|
|
228
|
+
const existing = committed.find((p) => p.id === provider.id);
|
|
215
229
|
if (existing === void 0) return {
|
|
216
230
|
ok: false,
|
|
217
231
|
error: `provider id not found: ${provider.id}`
|
|
218
232
|
};
|
|
219
233
|
const stored = {
|
|
220
|
-
|
|
221
|
-
name: provider.name ?? "",
|
|
222
|
-
endpoint: provider.endpoint ?? "stub://aigc-backend",
|
|
223
|
-
apiKey: provider.apiKey ?? "",
|
|
224
|
-
instructions: provider.instructions ?? "",
|
|
234
|
+
...resolveStored(provider),
|
|
225
235
|
auth: {
|
|
226
236
|
scheme: provider.auth?.scheme ?? existing.auth.scheme,
|
|
227
237
|
name: provider.auth?.name ?? existing.auth.name
|
|
228
238
|
},
|
|
229
239
|
builtin: existing.builtin
|
|
230
240
|
};
|
|
231
|
-
this.
|
|
232
|
-
this.persist();
|
|
233
|
-
return {
|
|
234
|
-
ok: true,
|
|
235
|
-
providers: this.list()
|
|
236
|
-
};
|
|
241
|
+
return this.commit(committed.map((p) => p.id === stored.id ? stored : p));
|
|
237
242
|
}
|
|
238
243
|
/**
|
|
239
244
|
* Replace a provider's usage instructions (called by the model's
|
|
240
245
|
* aigc_provider_set_instructions tool after it probes the API).
|
|
241
246
|
*/
|
|
242
|
-
setInstructions(id, instructions) {
|
|
243
|
-
const
|
|
244
|
-
if (
|
|
247
|
+
async setInstructions(id, instructions) {
|
|
248
|
+
const committed = this.committed();
|
|
249
|
+
if (committed.find((p) => p.id === id) === void 0) return {
|
|
245
250
|
ok: false,
|
|
246
251
|
error: `provider id not found: ${id}`
|
|
247
252
|
};
|
|
248
|
-
|
|
249
|
-
...
|
|
253
|
+
return this.commit(committed.map((p) => p.id === id ? {
|
|
254
|
+
...p,
|
|
250
255
|
instructions
|
|
251
|
-
};
|
|
252
|
-
this.providers.set(stored.id, stored);
|
|
253
|
-
this.persist();
|
|
254
|
-
return {
|
|
255
|
-
ok: true,
|
|
256
|
-
providers: this.list()
|
|
257
|
-
};
|
|
256
|
+
} : p));
|
|
258
257
|
}
|
|
259
258
|
/** Remove a provider. Returns failure for unknown id. */
|
|
260
|
-
remove(id) {
|
|
261
|
-
|
|
259
|
+
async remove(id) {
|
|
260
|
+
const committed = this.committed();
|
|
261
|
+
if (!committed.some((p) => p.id === id)) return {
|
|
262
262
|
ok: false,
|
|
263
263
|
error: `provider id not found: ${id}`
|
|
264
264
|
};
|
|
265
|
-
this.
|
|
266
|
-
return {
|
|
267
|
-
ok: true,
|
|
268
|
-
providers: this.list()
|
|
269
|
-
};
|
|
265
|
+
return this.commit(committed.filter((p) => p.id !== id));
|
|
270
266
|
}
|
|
271
267
|
/**
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
* ends with the latest state on disk.
|
|
268
|
+
* Commit a replacement list: through the persistence face when attached
|
|
269
|
+
* (the post-commit committed value is reported), otherwise into the
|
|
270
|
+
* in-memory map. A refused persistence write leaves the committed state
|
|
271
|
+
* untouched and reports `{ ok: false }`.
|
|
277
272
|
*/
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
273
|
+
async commit(next) {
|
|
274
|
+
if (this.persistence === void 0) {
|
|
275
|
+
this.providers.clear();
|
|
276
|
+
for (const provider of resolveAigcProviders(next)) this.providers.set(provider.id, provider);
|
|
277
|
+
return {
|
|
278
|
+
ok: true,
|
|
279
|
+
providers: this.list()
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
await this.persistence.persist(next);
|
|
284
|
+
return {
|
|
285
|
+
ok: true,
|
|
286
|
+
providers: this.list()
|
|
287
|
+
};
|
|
288
|
+
} catch (error) {
|
|
289
|
+
return {
|
|
290
|
+
ok: false,
|
|
291
|
+
error: error instanceof Error ? error.message : String(error)
|
|
292
|
+
};
|
|
293
|
+
}
|
|
281
294
|
}
|
|
282
295
|
};
|
|
283
|
-
/**
|
|
284
|
-
function
|
|
296
|
+
/** Normalize one mutation input into its resolved stored shape. */
|
|
297
|
+
function resolveStored(provider) {
|
|
298
|
+
const auth = provider.auth ?? {};
|
|
299
|
+
return {
|
|
300
|
+
id: provider.id,
|
|
301
|
+
name: provider.name ?? "",
|
|
302
|
+
endpoint: provider.endpoint ?? "stub://aigc-backend",
|
|
303
|
+
apiKey: provider.apiKey ?? "",
|
|
304
|
+
instructions: provider.instructions ?? "",
|
|
305
|
+
auth: {
|
|
306
|
+
scheme: auth.scheme ?? "bearer",
|
|
307
|
+
name: auth.name ?? ""
|
|
308
|
+
},
|
|
309
|
+
builtin: provider.builtin ?? false
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
//#endregion
|
|
313
|
+
//#region src/settings.ts
|
|
314
|
+
/**
|
|
315
|
+
* settings.ts — host-side bridge between the entry's profile-owned Config
|
|
316
|
+
* and the plugin's provider store, plus the one-time legacy import.
|
|
317
|
+
*
|
|
318
|
+
* dsh 0.1.7-rc.1 (`DSH-0.1.7-J1-04`) removed the namespace-registration API:
|
|
319
|
+
* a plugin declares a Cordis `Config` (see `config.ts`, `providers` marked
|
|
320
|
+
* `.volatile()`) and only declares the presentation policy for its own page.
|
|
321
|
+
* The bridge exposes:
|
|
322
|
+
*
|
|
323
|
+
* - `source()`: the raw committed provider list, read from the live
|
|
324
|
+
* volatile reference on every call, so the tools and RPC surface always
|
|
325
|
+
* serve the latest accepted value (including edits written by other
|
|
326
|
+
* surfaces through the settings service).
|
|
327
|
+
* - `persist(providers)`: commits a replacement list through the settings
|
|
328
|
+
* service, which writes it into the active profile's `cordis.patch.yml`
|
|
329
|
+
* under the entry id `dsh-aigc-canvas` and updates the live reference
|
|
330
|
+
* in place.
|
|
331
|
+
* - `writable`: whether a settings provider is mounted.
|
|
332
|
+
*
|
|
333
|
+
* On first mount the legacy persistence file of pre-0.1.11 versions
|
|
334
|
+
* (`~/.dsh/aigc-canvas/providers.json`) is imported once (the file is
|
|
335
|
+
* renamed before the import, so a partial import never repeats) and left
|
|
336
|
+
* behind as `providers.json.imported`.
|
|
337
|
+
*
|
|
338
|
+
* @module @huanlin/dsh-plugin-aigc-canvas/settings
|
|
339
|
+
*/
|
|
340
|
+
/** Profile entry id under which the provider list persists (`cordis.patch.yml`). */
|
|
341
|
+
const SETTINGS_NAMESPACE = "dsh-aigc-canvas";
|
|
342
|
+
/** Default legacy persistence path (pre-0.1.11 custom file). */
|
|
343
|
+
const LEGACY_PROVIDERS_JSON = join(homedir(), ".dsh", "aigc-canvas", "providers.json");
|
|
344
|
+
/**
|
|
345
|
+
* Declare the plugin's settings presentation policy and return the bridge.
|
|
346
|
+
*
|
|
347
|
+
* `auto: false` suppresses the schema-generated page: this plugin ships its
|
|
348
|
+
* own editor as the bundle row's `plugins.row.config` entry on the Plugins
|
|
349
|
+
* page.
|
|
350
|
+
*
|
|
351
|
+
* @param ctx - host context.
|
|
352
|
+
* @param entry - the entry's volatile Cordis config.
|
|
353
|
+
* @param options - optional overrides (legacy import path, attach callback).
|
|
354
|
+
* @param options.legacyPath - path of the legacy providers file to import once.
|
|
355
|
+
* @param options.onReady - invoked when the settings provider is mounted (before the legacy import settles).
|
|
356
|
+
* @returns the bridge the provider store consumes.
|
|
357
|
+
*/
|
|
358
|
+
function installAigcSettings(ctx, entry, options = {}) {
|
|
359
|
+
let settings;
|
|
360
|
+
ctx.inject(["settings"], (sctx) => {
|
|
361
|
+
settings = sctx.settings;
|
|
362
|
+
sctx.effect(() => sctx.settings.configure({ auto: false }, ctx.fiber));
|
|
363
|
+
options.onReady?.();
|
|
364
|
+
importLegacyProviders(options.legacyPath ?? LEGACY_PROVIDERS_JSON, sctx.settings, ctx.logger);
|
|
365
|
+
return () => {
|
|
366
|
+
settings = void 0;
|
|
367
|
+
};
|
|
368
|
+
});
|
|
369
|
+
return {
|
|
370
|
+
source: () => {
|
|
371
|
+
const raw = entry?.providers?.get();
|
|
372
|
+
return Array.isArray(raw) ? raw : [];
|
|
373
|
+
},
|
|
374
|
+
persist: async (providers) => {
|
|
375
|
+
if (settings === void 0) return;
|
|
376
|
+
await settings.update(SETTINGS_NAMESPACE, { providers });
|
|
377
|
+
},
|
|
378
|
+
get writable() {
|
|
379
|
+
return settings !== void 0;
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Import the legacy `providers.json` once: rename the file first (so a
|
|
385
|
+
* partial import never repeats), then commit the parsed list into the
|
|
386
|
+
* profile entry config. Failures are logged, never thrown.
|
|
387
|
+
*
|
|
388
|
+
* @param path - the legacy file path.
|
|
389
|
+
* @param settings - the mounted settings service.
|
|
390
|
+
* @param logger - the host logger.
|
|
391
|
+
*/
|
|
392
|
+
async function importLegacyProviders(path, settings, logger) {
|
|
285
393
|
try {
|
|
286
|
-
|
|
287
|
-
const
|
|
394
|
+
if (!existsSync(path)) return;
|
|
395
|
+
const imported = `${path}.imported`;
|
|
396
|
+
await rm(imported, { force: true });
|
|
397
|
+
await rename(path, imported);
|
|
398
|
+
const providers = loadLegacyProviders(imported);
|
|
399
|
+
if (providers === null) return;
|
|
400
|
+
await settings.update(SETTINGS_NAMESPACE, { providers });
|
|
401
|
+
logger.info(`dsh-aigc-canvas: imported ${imported} into the profile entry config`);
|
|
402
|
+
} catch (error) {
|
|
403
|
+
logger.warn("dsh-aigc-canvas: legacy providers.json import failed:", error);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Read a legacy providers JSON document; null when absent or unreadable.
|
|
408
|
+
* Mirrors the pre-0.1.11 reader (defensive per-item coercion).
|
|
409
|
+
*/
|
|
410
|
+
function loadLegacyProviders(path) {
|
|
411
|
+
try {
|
|
412
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
288
413
|
if (!Array.isArray(parsed)) return null;
|
|
289
414
|
const providers = [];
|
|
290
415
|
for (const item of parsed) {
|
|
@@ -1903,7 +2028,7 @@ function mimeFromExt(filePath) {
|
|
|
1903
2028
|
*
|
|
1904
2029
|
* @param ctx - host plugin context (carries the tools service).
|
|
1905
2030
|
* @param getProvider - live provider getter (takes optional provider id).
|
|
1906
|
-
* @param setInstructions - persists usage instructions for one provider (the host's ProviderStore).
|
|
2031
|
+
* @param setInstructions - persists usage instructions for one provider (the host's ProviderStore; resolves after the profile-config commit).
|
|
1907
2032
|
* @param listProviders - returns info for all providers (for aigc_get_provider_info).
|
|
1908
2033
|
* @param canvas - the canvas registry service (host-owned state).
|
|
1909
2034
|
* @param resolveCwd - live cwd resolver for one session id.
|
|
@@ -2238,16 +2363,16 @@ function registerTools(ctx, getProvider, setInstructions, listProviders, canvas,
|
|
|
2238
2363
|
},
|
|
2239
2364
|
render: textRender((v) => `Saved usage instructions for provider "${v.provider_id}".`)
|
|
2240
2365
|
},
|
|
2241
|
-
execute: (args) => {
|
|
2366
|
+
execute: async (args) => {
|
|
2242
2367
|
if (typeof args.provider_id !== "string" || args.provider_id === "") throw new AigcError("bad-request", "provider_id is required");
|
|
2243
2368
|
if (typeof args.instructions !== "string" || args.instructions === "") throw new AigcError("bad-request", "instructions is required");
|
|
2244
2369
|
getProvider(args.provider_id);
|
|
2245
|
-
const result = setInstructions(args.provider_id, args.instructions);
|
|
2370
|
+
const result = await setInstructions(args.provider_id, args.instructions);
|
|
2246
2371
|
if (!result.ok) throw new AigcError("bad-request", result.error ?? "cannot save instructions");
|
|
2247
|
-
return
|
|
2372
|
+
return {
|
|
2248
2373
|
ok: true,
|
|
2249
2374
|
provider_id: args.provider_id
|
|
2250
|
-
}
|
|
2375
|
+
};
|
|
2251
2376
|
}
|
|
2252
2377
|
}));
|
|
2253
2378
|
register(defineTool({
|
|
@@ -2721,10 +2846,10 @@ async function saveResponseToSession(content, ext, sessionId, cwd) {
|
|
|
2721
2846
|
//#region src/index.ts
|
|
2722
2847
|
/**
|
|
2723
2848
|
* @huanlin/dsh-plugin-aigc-canvas host half: the canvas registry, the provider
|
|
2724
|
-
* store (
|
|
2725
|
-
* `/aigc-canvas/api/*` JSON API
|
|
2726
|
-
* `/aigc-canvas/file` media route +
|
|
2727
|
-
* and the `ctx.aigcCanvas` service.
|
|
2849
|
+
* store (the entry's profile-owned Config + per-provider usage
|
|
2850
|
+
* instructions), the fenced `/aigc-canvas/api/*` JSON API
|
|
2851
|
+
* (canvas.list/move/delete/upload) + `/aigc-canvas/file` media route +
|
|
2852
|
+
* `/aigc-canvas/ws/canvas` push WebSocket, and the `ctx.aigcCanvas` service.
|
|
2728
2853
|
*
|
|
2729
2854
|
* Model-facing tools (see tools.ts): aigc_get_provider_info, the generic
|
|
2730
2855
|
* aigc_http_request (auto-attaches endpoint + apiKey per provider config),
|
|
@@ -2732,10 +2857,13 @@ async function saveResponseToSession(content, ext, sessionId, cwd) {
|
|
|
2732
2857
|
* probing the API), aigc_canvas_place / aigc_canvas_link / aigc_canvas_unlink
|
|
2733
2858
|
* (put files on the free canvas), and aigc_canvas_list_elements.
|
|
2734
2859
|
*
|
|
2735
|
-
* Provider config is
|
|
2736
|
-
*
|
|
2737
|
-
*
|
|
2738
|
-
*
|
|
2860
|
+
* Provider config is profile-owned (dsh 0.1.7-rc.1 DSH-0.1.7-J1-04): the
|
|
2861
|
+
* `providers` field of the entry's Cordis Config is `.volatile()` and
|
|
2862
|
+
* persists per profile in `cordis.patch.yml` under the entry id
|
|
2863
|
+
* `dsh-aigc-canvas`. The settings page reads/writes it through the client
|
|
2864
|
+
* `configForms` transport (DSH-0.1.7-J1-27); the model's instruction tool
|
|
2865
|
+
* writes through the same settings bridge. Tools read the provider through
|
|
2866
|
+
* a getter so they always see the latest committed configuration.
|
|
2739
2867
|
*/
|
|
2740
2868
|
/** Plugin identity for cordis.yml rows. */
|
|
2741
2869
|
const name = "dsh-aigc-canvas";
|
|
@@ -2764,13 +2892,6 @@ function sessionCwdOf(ctx, sessionId) {
|
|
|
2764
2892
|
if (headerCwd !== void 0 && headerCwd !== "") return headerCwd;
|
|
2765
2893
|
throw new AigcError("not-found", `session "${sessionId}" is not registered or has no cwd yet`, 404);
|
|
2766
2894
|
}
|
|
2767
|
-
/** Convert a resolved config to the runtime global settings wire shape. */
|
|
2768
|
-
function toGlobalSettings(resolved) {
|
|
2769
|
-
return {
|
|
2770
|
-
requestTimeoutMs: resolved.requestTimeoutMs,
|
|
2771
|
-
mediaSizeLimit: resolved.mediaSizeLimit
|
|
2772
|
-
};
|
|
2773
|
-
}
|
|
2774
2895
|
/**
|
|
2775
2896
|
* Build a minimal user-role message and inject it into the agent's
|
|
2776
2897
|
* next-step context (non-waking). Used to notify the model of user-
|
|
@@ -2825,7 +2946,7 @@ function kindForExtension(ext) {
|
|
|
2825
2946
|
return "prompt";
|
|
2826
2947
|
}
|
|
2827
2948
|
/** Build the JSON API method table. */
|
|
2828
|
-
function buildApi(ctx, canvas,
|
|
2949
|
+
function buildApi(ctx, canvas, getMediaLimit) {
|
|
2829
2950
|
return {
|
|
2830
2951
|
"canvas.list": async (payload) => {
|
|
2831
2952
|
const sessionId = requireString(payload, "sessionId");
|
|
@@ -2862,7 +2983,7 @@ function buildApi(ctx, canvas, store, getResolved) {
|
|
|
2862
2983
|
const mediaBase64 = typeof record?.mediaBase64 === "string" ? record.mediaBase64 : "";
|
|
2863
2984
|
if (fileName === "" || mediaBase64 === "") throw new AigcError("bad-request", "fileName and mediaBase64 are required strings");
|
|
2864
2985
|
const bytes = Buffer.from(mediaBase64, "base64");
|
|
2865
|
-
if (bytes.byteLength >
|
|
2986
|
+
if (bytes.byteLength > getMediaLimit()) throw new AigcError("fs-error", `uploaded file too large (${bytes.byteLength} bytes)`);
|
|
2866
2987
|
const cwd = sessionCwdOf(ctx, sessionId);
|
|
2867
2988
|
const dir = canvasDirFor(cwd, sessionId);
|
|
2868
2989
|
await mkdir(dir, { recursive: true });
|
|
@@ -2887,36 +3008,6 @@ function buildApi(ctx, canvas, store, getResolved) {
|
|
|
2887
3008
|
ok: true,
|
|
2888
3009
|
element: el
|
|
2889
3010
|
};
|
|
2890
|
-
},
|
|
2891
|
-
"providers.list": () => {
|
|
2892
|
-
return { providers: store.list() };
|
|
2893
|
-
},
|
|
2894
|
-
"providers.add": (payload) => {
|
|
2895
|
-
const provider = payload?.provider;
|
|
2896
|
-
if (provider === null || typeof provider !== "object" || Array.isArray(provider)) throw new AigcError("bad-request", "expected { provider: AigcProvider }");
|
|
2897
|
-
const result = store.add(provider);
|
|
2898
|
-
if (!result.ok) throw new AigcError("bad-request", result.error);
|
|
2899
|
-
return { providers: result.providers };
|
|
2900
|
-
},
|
|
2901
|
-
"providers.update": (payload) => {
|
|
2902
|
-
const provider = payload?.provider;
|
|
2903
|
-
if (provider === null || typeof provider !== "object" || Array.isArray(provider)) throw new AigcError("bad-request", "expected { provider: AigcProvider }");
|
|
2904
|
-
const result = store.update(provider);
|
|
2905
|
-
if (!result.ok) throw new AigcError("bad-request", result.error);
|
|
2906
|
-
return { providers: result.providers };
|
|
2907
|
-
},
|
|
2908
|
-
"providers.remove": (payload) => {
|
|
2909
|
-
const id = payload?.id;
|
|
2910
|
-
if (typeof id !== "string" || id === "") throw new AigcError("bad-request", "expected { id: string }");
|
|
2911
|
-
const result = store.remove(id);
|
|
2912
|
-
if (!result.ok) throw new AigcError("bad-request", result.error);
|
|
2913
|
-
return { providers: result.providers };
|
|
2914
|
-
},
|
|
2915
|
-
"config.get": () => {
|
|
2916
|
-
return {
|
|
2917
|
-
...toGlobalSettings(getResolved()),
|
|
2918
|
-
providers: store.list()
|
|
2919
|
-
};
|
|
2920
3011
|
}
|
|
2921
3012
|
};
|
|
2922
3013
|
}
|
|
@@ -2929,11 +3020,9 @@ function apply(ctx, config) {
|
|
|
2929
3020
|
const canvas = createAigcCanvasService((sessionId) => sessionCwdOf(ctx, sessionId), mediaLimit);
|
|
2930
3021
|
ctx.provide("aigcCanvas", canvas);
|
|
2931
3022
|
const store = new ProviderStore(resolved.providers);
|
|
2932
|
-
const
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
mediaSizeLimit: resolved.mediaSizeLimit
|
|
2936
|
-
});
|
|
3023
|
+
const bridge = installAigcSettings(ctx, config, { onReady: () => {
|
|
3024
|
+
store.attachPersistence(bridge);
|
|
3025
|
+
} });
|
|
2937
3026
|
const getProvider = (providerId) => {
|
|
2938
3027
|
if (providerId !== void 0 && providerId !== "") {
|
|
2939
3028
|
const provider = store.get(providerId);
|
|
@@ -2956,7 +3045,7 @@ function apply(ctx, config) {
|
|
|
2956
3045
|
isDefault: p.id === defaultId
|
|
2957
3046
|
}));
|
|
2958
3047
|
};
|
|
2959
|
-
const api = buildApi(ctx, canvas,
|
|
3048
|
+
const api = buildApi(ctx, canvas, mediaLimit);
|
|
2960
3049
|
ctx.effect(() => ctx.webServer.register({
|
|
2961
3050
|
kind: "prefix",
|
|
2962
3051
|
path: "/aigc-canvas/api",
|
|
@@ -3103,4 +3192,4 @@ async function attachCanvasPush(canvas, ws, req) {
|
|
|
3103
3192
|
}
|
|
3104
3193
|
}
|
|
3105
3194
|
//#endregion
|
|
3106
|
-
export { Config, apply, inject, name };
|
|
3195
|
+
export { Config, SETTINGS_NAMESPACE, apply, inject, name };
|