@michengai/dsh-codex-pet 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 +20 -0
- package/CHANGELOG.zh-CN.md +20 -0
- package/README.md +208 -0
- package/README.zh-CN.md +210 -0
- package/assets/branding/dsh-codex-pet-banner.png +0 -0
- package/assets/codex/bsod/spritesheet.webp +0 -0
- package/assets/codex/codex/spritesheet.webp +0 -0
- package/assets/codex/dewey/spritesheet.webp +0 -0
- package/assets/codex/fireball/spritesheet.webp +0 -0
- package/assets/codex/hoots/spritesheet.webp +0 -0
- package/assets/codex/null-signal/spritesheet.webp +0 -0
- package/assets/codex/rocky/spritesheet.webp +0 -0
- package/assets/codex/seedy/spritesheet.webp +0 -0
- package/assets/codex/stacky/spritesheet.webp +0 -0
- package/assets/screenshots/pet-conversation.png +0 -0
- package/assets/screenshots/pet-notifications.png +0 -0
- package/assets/screenshots/pet-settings.png +0 -0
- package/cordis.patch.yml +3 -0
- package/lib/client.js +2181 -0
- package/lib/index.js +777 -0
- package/package.json +78 -0
- package/skills/hatch-pet/SKILL.md +37 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,777 @@
|
|
|
1
|
+
// src/update-errors.ts
|
|
2
|
+
var messages = {
|
|
3
|
+
"INVALID_VERSION": [
|
|
4
|
+
"\u65E0\u6CD5\u8BFB\u53D6\u5F53\u524D\u63D2\u4EF6\u7248\u672C\u3002",
|
|
5
|
+
"Could not read the current plugin version."
|
|
6
|
+
],
|
|
7
|
+
"AUTO_UPDATE_UNAVAILABLE": [
|
|
8
|
+
"\u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301\u81EA\u52A8\u66F4\u65B0\uFF0C\u8BF7\u4F7F\u7528\u624B\u5DE5\u66F4\u65B0\u547D\u4EE4\u3002",
|
|
9
|
+
"Automatic updates are unavailable. Use the manual update command."
|
|
10
|
+
],
|
|
11
|
+
"UPDATE_TIMEOUT": [
|
|
12
|
+
"\u66F4\u65B0\u8D85\u65F6\uFF0C\u5DF2\u8BF7\u6C42\u53D6\u6D88\uFF1B\u8FDB\u7A0B\u7ED3\u675F\u524D\u4E0D\u80FD\u518D\u6B21\u5B89\u88C5\u3002",
|
|
13
|
+
"The update timed out and cancellation was requested. Wait for the process to exit before retrying."
|
|
14
|
+
],
|
|
15
|
+
"UNTRUSTED_REQUEST": [
|
|
16
|
+
"\u5DF2\u62D2\u7EDD\u975E\u672C\u673A\u540C\u6E90\u66F4\u65B0\u8BF7\u6C42\u3002",
|
|
17
|
+
"The update request was rejected because it is not from the same local origin."
|
|
18
|
+
],
|
|
19
|
+
"UPDATE_IN_PROGRESS": [
|
|
20
|
+
"\u5F53\u524D\u63D2\u4EF6\u6B63\u5728\u66F4\u65B0\uFF0C\u8BF7\u7A0D\u5019\u3002",
|
|
21
|
+
"The plugin is being updated. Please wait."
|
|
22
|
+
],
|
|
23
|
+
"NOT_PUBLISHED": [
|
|
24
|
+
"\u63D2\u4EF6\u5C1A\u672A\u53D1\u5E03\u5230 npm\u3002",
|
|
25
|
+
"The plugin has not been published to npm yet."
|
|
26
|
+
],
|
|
27
|
+
"REGISTRY_UNAVAILABLE": [
|
|
28
|
+
"\u6682\u65F6\u65E0\u6CD5\u83B7\u53D6\u6700\u65B0\u7248\u672C\u3002",
|
|
29
|
+
"Could not retrieve the latest version. Please try again later."
|
|
30
|
+
],
|
|
31
|
+
"UPDATE_FAILED": [
|
|
32
|
+
"\u66F4\u65B0\u5931\u8D25\uFF0C\u8BF7\u67E5\u770B\u670D\u52A1\u7AEF\u65E5\u5FD7\u3002",
|
|
33
|
+
"The update failed. Check the server logs."
|
|
34
|
+
]
|
|
35
|
+
};
|
|
36
|
+
function updateErrorMessage(code, language) {
|
|
37
|
+
const key = typeof code === "string" && Object.hasOwn(messages, code) ? code : "UPDATE_FAILED";
|
|
38
|
+
return messages[key][/^zh(?:-|$)/i.test(language) ? 0 : 1];
|
|
39
|
+
}
|
|
40
|
+
var UpdateFailure = class extends Error {
|
|
41
|
+
constructor(code, detail) {
|
|
42
|
+
super(detail ?? updateErrorMessage(code, "zh"));
|
|
43
|
+
this.code = code;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
function updateErrorPayload(code) {
|
|
47
|
+
return { code, error: updateErrorMessage(code, "zh") };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/plugin-updater.ts
|
|
51
|
+
import { spawn } from "node:child_process";
|
|
52
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
53
|
+
import { readFile } from "node:fs/promises";
|
|
54
|
+
import { homedir } from "node:os";
|
|
55
|
+
import { basename, dirname, isAbsolute, resolve } from "node:path";
|
|
56
|
+
import { fileURLToPath } from "node:url";
|
|
57
|
+
var PLUGIN_UPDATE_HEADER = "x-michengai-plugin-update";
|
|
58
|
+
function header(request, name2) {
|
|
59
|
+
const value = request.headers?.[name2];
|
|
60
|
+
return Array.isArray(value) ? value[0] : value;
|
|
61
|
+
}
|
|
62
|
+
function isLoopbackAddress(value) {
|
|
63
|
+
const address = value?.toLowerCase().replace(/^\[|\]$/g, "");
|
|
64
|
+
return address === "localhost" || address === "localhost." || address === "::1" || address?.startsWith("127.") === true || address?.startsWith("::ffff:127.") === true;
|
|
65
|
+
}
|
|
66
|
+
function isTrustedUpdateRequest(request) {
|
|
67
|
+
if (header(request, PLUGIN_UPDATE_HEADER) !== "1") return false;
|
|
68
|
+
if (!isLoopbackAddress(request.socket?.remoteAddress)) return false;
|
|
69
|
+
const site = header(request, "sec-fetch-site");
|
|
70
|
+
if (site !== void 0 && site !== "same-origin") return false;
|
|
71
|
+
const origin = header(request, "origin");
|
|
72
|
+
const host = header(request, "host");
|
|
73
|
+
if (origin === void 0 || host === void 0) return false;
|
|
74
|
+
try {
|
|
75
|
+
const url = new URL(origin);
|
|
76
|
+
return (url.protocol === "http:" || url.protocol === "https:") && isLoopbackAddress(url.hostname) && url.host === host;
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function validProfileName(value) {
|
|
82
|
+
return typeof value === "string" && value !== "" && value !== "." && value !== ".." && !value.includes("/") && !value.includes("\\") && !/[\0-\x1f\x7f]/.test(value);
|
|
83
|
+
}
|
|
84
|
+
function profileNameFromArgv(argv) {
|
|
85
|
+
for (let index = 2; index < argv.length; index += 1) {
|
|
86
|
+
if (argv[index] === "--profile") return argv[index + 1];
|
|
87
|
+
if (argv[index]?.startsWith("--profile="))
|
|
88
|
+
return argv[index].slice("--profile=".length);
|
|
89
|
+
}
|
|
90
|
+
return argv[2] === "web" ? "web" : void 0;
|
|
91
|
+
}
|
|
92
|
+
function isDshCliEntry(entry, manifest, packageRoot2) {
|
|
93
|
+
if (typeof manifest !== "object" || manifest === null) return false;
|
|
94
|
+
const value = manifest;
|
|
95
|
+
if (value.name !== "@deepseek-ai/dsh") return false;
|
|
96
|
+
const bin = typeof value.bin === "string" ? value.bin : typeof value.bin === "object" && value.bin !== null ? value.bin.dsh : void 0;
|
|
97
|
+
return typeof bin === "string" && bin !== "" && !isAbsolute(bin) && resolve(packageRoot2, bin) === resolve(entry);
|
|
98
|
+
}
|
|
99
|
+
function cliEntry() {
|
|
100
|
+
const value = process.argv[1];
|
|
101
|
+
if (value === void 0 || value === "") return void 0;
|
|
102
|
+
const entry = value.startsWith("file:") ? fileURLToPath(value) : resolve(process.cwd(), value);
|
|
103
|
+
if (!existsSync(entry)) return void 0;
|
|
104
|
+
for (let directory = dirname(entry); ; ) {
|
|
105
|
+
const manifestPath = resolve(directory, "package.json");
|
|
106
|
+
if (existsSync(manifestPath)) {
|
|
107
|
+
try {
|
|
108
|
+
if (isDshCliEntry(
|
|
109
|
+
entry,
|
|
110
|
+
JSON.parse(readFileSync(manifestPath, "utf8")),
|
|
111
|
+
directory
|
|
112
|
+
))
|
|
113
|
+
return entry;
|
|
114
|
+
} catch {
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const parent = dirname(directory);
|
|
118
|
+
if (parent === directory) return void 0;
|
|
119
|
+
directory = parent;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function runtime() {
|
|
123
|
+
const profileDir = resolve(
|
|
124
|
+
process.env.DSH_PROFILE_DIR ?? resolve(homedir(), ".dsh", "profiles", "web")
|
|
125
|
+
);
|
|
126
|
+
const selected = profileNameFromArgv(process.argv);
|
|
127
|
+
const profileName = validProfileName(selected) ? selected : validProfileName(basename(profileDir)) ? basename(profileDir) : "web";
|
|
128
|
+
const entry = cliEntry();
|
|
129
|
+
return {
|
|
130
|
+
profileName,
|
|
131
|
+
profileDir,
|
|
132
|
+
...entry === void 0 ? {} : { cliEntry: entry }
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function parseSemver(value) {
|
|
136
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(
|
|
137
|
+
value
|
|
138
|
+
);
|
|
139
|
+
if (match === null) return void 0;
|
|
140
|
+
return {
|
|
141
|
+
core: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
142
|
+
prerelease: match[4]?.split(".") ?? []
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function isNewerVersion(currentValue, candidateValue) {
|
|
146
|
+
const current = parseSemver(currentValue);
|
|
147
|
+
const candidate = parseSemver(candidateValue);
|
|
148
|
+
if (current === void 0 || candidate === void 0) return false;
|
|
149
|
+
for (let index = 0; index < 3; index += 1) {
|
|
150
|
+
if (candidate.core[index] !== current.core[index])
|
|
151
|
+
return candidate.core[index] > current.core[index];
|
|
152
|
+
}
|
|
153
|
+
return comparePrerelease(candidate.prerelease, current.prerelease) > 0;
|
|
154
|
+
}
|
|
155
|
+
function comparePrerelease(left, right) {
|
|
156
|
+
if (left.length === 0 || right.length === 0)
|
|
157
|
+
return left.length === right.length ? 0 : left.length === 0 ? 1 : -1;
|
|
158
|
+
const length = Math.max(left.length, right.length);
|
|
159
|
+
for (let index = 0; index < length; index += 1) {
|
|
160
|
+
const a = left[index];
|
|
161
|
+
const b = right[index];
|
|
162
|
+
if (a === void 0 || b === void 0)
|
|
163
|
+
return a === b ? 0 : a === void 0 ? -1 : 1;
|
|
164
|
+
if (a === b) continue;
|
|
165
|
+
const aNumeric = /^\d+$/.test(a);
|
|
166
|
+
const bNumeric = /^\d+$/.test(b);
|
|
167
|
+
if (aNumeric && bNumeric) {
|
|
168
|
+
const aNumber = BigInt(a);
|
|
169
|
+
const bNumber = BigInt(b);
|
|
170
|
+
if (aNumber !== bNumber) return aNumber > bNumber ? 1 : -1;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (aNumeric !== bNumeric) return aNumeric ? -1 : 1;
|
|
174
|
+
return a > b ? 1 : -1;
|
|
175
|
+
}
|
|
176
|
+
return 0;
|
|
177
|
+
}
|
|
178
|
+
var latestCache;
|
|
179
|
+
async function latestVersion(packageName) {
|
|
180
|
+
if (latestCache?.packageName === packageName && Date.now() < latestCache.expiresAt)
|
|
181
|
+
return latestCache.version;
|
|
182
|
+
try {
|
|
183
|
+
const response = await fetch(
|
|
184
|
+
`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`,
|
|
185
|
+
{ signal: AbortSignal.timeout(8e3) }
|
|
186
|
+
);
|
|
187
|
+
if (response.status === 404) return null;
|
|
188
|
+
if (!response.ok) return void 0;
|
|
189
|
+
const value = await response.json();
|
|
190
|
+
if (typeof value.version !== "string" || !parseSemver(value.version))
|
|
191
|
+
return void 0;
|
|
192
|
+
latestCache = {
|
|
193
|
+
packageName,
|
|
194
|
+
version: value.version,
|
|
195
|
+
expiresAt: Date.now() + 5 * 6e4
|
|
196
|
+
};
|
|
197
|
+
return value.version;
|
|
198
|
+
} catch {
|
|
199
|
+
return void 0;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async function currentVersion(manifestUrl) {
|
|
203
|
+
const value = JSON.parse(await readFile(manifestUrl, "utf8"));
|
|
204
|
+
if (typeof value.version !== "string" || !parseSemver(value.version))
|
|
205
|
+
throw new UpdateFailure("INVALID_VERSION");
|
|
206
|
+
return value.version;
|
|
207
|
+
}
|
|
208
|
+
async function status(options, target) {
|
|
209
|
+
const current = await currentVersion(options.manifestUrl);
|
|
210
|
+
const latest = await latestVersion(options.packageName);
|
|
211
|
+
return {
|
|
212
|
+
packageName: options.packageName,
|
|
213
|
+
currentVersion: current,
|
|
214
|
+
...latest == null ? {} : { latestVersion: latest },
|
|
215
|
+
notPublished: latest === null,
|
|
216
|
+
latestCheckFailed: latest === void 0,
|
|
217
|
+
updateAvailable: latest != null && isNewerVersion(current, latest),
|
|
218
|
+
profileName: target.profileName,
|
|
219
|
+
canAutoUpdate: target.cliEntry !== void 0
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
async function install(target, packageSpec, track) {
|
|
223
|
+
if (!target.cliEntry) throw new UpdateFailure("AUTO_UPDATE_UNAVAILABLE");
|
|
224
|
+
const child = spawn(
|
|
225
|
+
process.execPath,
|
|
226
|
+
[
|
|
227
|
+
target.cliEntry,
|
|
228
|
+
"plugin",
|
|
229
|
+
"--profile",
|
|
230
|
+
target.profileName,
|
|
231
|
+
"add",
|
|
232
|
+
"--config.minimumReleaseAge=0",
|
|
233
|
+
packageSpec,
|
|
234
|
+
"--registry=https://registry.npmjs.org/"
|
|
235
|
+
],
|
|
236
|
+
{
|
|
237
|
+
cwd: target.profileDir,
|
|
238
|
+
windowsHide: true,
|
|
239
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
240
|
+
env: { ...process.env, NO_COLOR: "1" }
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
let detail = "";
|
|
244
|
+
child.stdout.on("data", (chunk) => {
|
|
245
|
+
detail = (detail + String(chunk)).slice(-4e3);
|
|
246
|
+
});
|
|
247
|
+
child.stderr.on("data", (chunk) => {
|
|
248
|
+
detail = (detail + String(chunk)).slice(-4e3);
|
|
249
|
+
});
|
|
250
|
+
const done = new Promise((resolve3, reject) => {
|
|
251
|
+
child.once("error", reject);
|
|
252
|
+
child.once(
|
|
253
|
+
"close",
|
|
254
|
+
(code) => code === 0 ? resolve3() : reject(
|
|
255
|
+
new UpdateFailure(
|
|
256
|
+
"UPDATE_FAILED",
|
|
257
|
+
detail.trim() || `\u66F4\u65B0\u8FDB\u7A0B\u9000\u51FA\u7801 ${code}`
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
track(done);
|
|
263
|
+
let timer;
|
|
264
|
+
try {
|
|
265
|
+
await Promise.race([
|
|
266
|
+
done,
|
|
267
|
+
new Promise((_, reject) => {
|
|
268
|
+
timer = setTimeout(() => {
|
|
269
|
+
child.kill();
|
|
270
|
+
reject(new UpdateFailure("UPDATE_TIMEOUT"));
|
|
271
|
+
}, 10 * 6e4);
|
|
272
|
+
})
|
|
273
|
+
]);
|
|
274
|
+
} finally {
|
|
275
|
+
clearTimeout(timer);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function json(response, statusCode, value) {
|
|
279
|
+
response.writeHead(statusCode, {
|
|
280
|
+
"content-type": "application/json; charset=utf-8",
|
|
281
|
+
"cache-control": "no-store"
|
|
282
|
+
});
|
|
283
|
+
response.end(JSON.stringify(value));
|
|
284
|
+
}
|
|
285
|
+
function registerPluginUpdater(ctx, options) {
|
|
286
|
+
const host = ctx;
|
|
287
|
+
let installing = false;
|
|
288
|
+
return host.webServer.register({
|
|
289
|
+
kind: "exact",
|
|
290
|
+
path: options.endpoint,
|
|
291
|
+
handler: async (request, response) => {
|
|
292
|
+
try {
|
|
293
|
+
const target = runtime();
|
|
294
|
+
if (request.method === "GET" || request.method === "HEAD") {
|
|
295
|
+
const payload = await status(options, target);
|
|
296
|
+
response.writeHead(200, {
|
|
297
|
+
"content-type": "application/json; charset=utf-8",
|
|
298
|
+
"cache-control": "no-store"
|
|
299
|
+
});
|
|
300
|
+
response.end(
|
|
301
|
+
request.method === "HEAD" ? void 0 : JSON.stringify(payload)
|
|
302
|
+
);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (request.method !== "POST") {
|
|
306
|
+
response.writeHead(405, { allow: "GET, HEAD, POST" });
|
|
307
|
+
response.end();
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (!isTrustedUpdateRequest(request)) {
|
|
311
|
+
json(response, 403, updateErrorPayload("UNTRUSTED_REQUEST"));
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (installing) {
|
|
315
|
+
json(response, 409, updateErrorPayload("UPDATE_IN_PROGRESS"));
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
installing = true;
|
|
319
|
+
let pendingInstall;
|
|
320
|
+
try {
|
|
321
|
+
const before = await status(options, target);
|
|
322
|
+
if (before.notPublished) {
|
|
323
|
+
json(response, 409, {
|
|
324
|
+
...before,
|
|
325
|
+
...updateErrorPayload("NOT_PUBLISHED")
|
|
326
|
+
});
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (before.latestVersion === void 0) {
|
|
330
|
+
json(response, 503, updateErrorPayload("REGISTRY_UNAVAILABLE"));
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (!before.updateAvailable) {
|
|
334
|
+
json(response, 200, before);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
await install(
|
|
338
|
+
target,
|
|
339
|
+
`${options.packageName}@${before.latestVersion}`,
|
|
340
|
+
(done) => {
|
|
341
|
+
pendingInstall = done;
|
|
342
|
+
}
|
|
343
|
+
);
|
|
344
|
+
json(response, 200, {
|
|
345
|
+
...before,
|
|
346
|
+
updatedVersion: before.latestVersion,
|
|
347
|
+
restartRequired: true,
|
|
348
|
+
autoReload: false
|
|
349
|
+
});
|
|
350
|
+
} finally {
|
|
351
|
+
if (pendingInstall)
|
|
352
|
+
void pendingInstall.then(
|
|
353
|
+
() => {
|
|
354
|
+
installing = false;
|
|
355
|
+
},
|
|
356
|
+
() => {
|
|
357
|
+
installing = false;
|
|
358
|
+
}
|
|
359
|
+
);
|
|
360
|
+
else installing = false;
|
|
361
|
+
}
|
|
362
|
+
} catch (error) {
|
|
363
|
+
ctx.logger.warn(`plugin updater failed: ${String(error)}`);
|
|
364
|
+
json(
|
|
365
|
+
response,
|
|
366
|
+
503,
|
|
367
|
+
updateErrorPayload(
|
|
368
|
+
error instanceof UpdateFailure ? error.code : "UPDATE_FAILED"
|
|
369
|
+
)
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// src/index.ts
|
|
377
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
378
|
+
import { mkdir as mkdir2 } from "node:fs/promises";
|
|
379
|
+
import { join as join2 } from "node:path";
|
|
380
|
+
import { spawn as spawn2, execFile } from "node:child_process";
|
|
381
|
+
|
|
382
|
+
// src/model.ts
|
|
383
|
+
var BASE = "/dsh-codex-pet";
|
|
384
|
+
var BUILTINS = [
|
|
385
|
+
["codex", "Codex", "The original Codex companion."],
|
|
386
|
+
["dewey", "Dewey", "A calm companion for focused workspace days."],
|
|
387
|
+
["fireball", "Fireball", "Hot path energy for fast iteration."],
|
|
388
|
+
["hoots", "Hoots", "A sharp-eyed owl for polished work in a blink."],
|
|
389
|
+
["rocky", "Rocky", "A steady rock when the diff gets large."],
|
|
390
|
+
["seedy", "Seedy", "Small green shoots for new ideas."],
|
|
391
|
+
["stacky", "Stacky", "A balanced stack for deep work."],
|
|
392
|
+
["bsod", "BSOD", "A tiny blue-screen gremlin."],
|
|
393
|
+
["null-signal", "Null Signal", "Quiet signal from the void."]
|
|
394
|
+
];
|
|
395
|
+
var DEFAULT_CONFIG = {
|
|
396
|
+
selected: "codex",
|
|
397
|
+
visible: true,
|
|
398
|
+
size: 120,
|
|
399
|
+
position: null
|
|
400
|
+
};
|
|
401
|
+
function normalizeConfig(value, previous = DEFAULT_CONFIG) {
|
|
402
|
+
if (!value || typeof value !== "object") throw new Error("\u914D\u7F6E\u5FC5\u987B\u662F\u5BF9\u8C61");
|
|
403
|
+
const data = value;
|
|
404
|
+
if (data.selected !== void 0 && (typeof data.selected !== "string" || !/^(builtin|custom):[\w-]+$|^[\w-]+$/.test(data.selected)))
|
|
405
|
+
throw new Error("\u5BA0\u7269\u6807\u8BC6\u65E0\u6548");
|
|
406
|
+
if (data.visible !== void 0 && typeof data.visible !== "boolean")
|
|
407
|
+
throw new Error("\u663E\u793A\u72B6\u6001\u65E0\u6548");
|
|
408
|
+
if (data.size !== void 0 && (!Number.isFinite(data.size) || data.size < 64 || data.size > 224))
|
|
409
|
+
throw new Error("\u5BA0\u7269\u5927\u5C0F\u987B\u4E3A 64\u2013224");
|
|
410
|
+
if (data.position !== void 0 && data.position !== null && (!Number.isFinite(data.position.x) || !Number.isFinite(data.position.y) || data.position.x < 0 || data.position.x > 1 || data.position.y < 0 || data.position.y > 1))
|
|
411
|
+
throw new Error("\u4F4D\u7F6E\u65E0\u6548");
|
|
412
|
+
return {
|
|
413
|
+
selected: data.selected ?? previous.selected,
|
|
414
|
+
visible: data.visible ?? previous.visible,
|
|
415
|
+
size: data.size ?? previous.size,
|
|
416
|
+
position: data.position === void 0 ? previous.position : data.position
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// src/library.ts
|
|
421
|
+
import { readFile as readFile2, readdir, realpath, stat, mkdir, writeFile, rename, unlink } from "node:fs/promises";
|
|
422
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
423
|
+
import { dirname as dirname2, join, resolve as resolve2, relative, isAbsolute as isAbsolute2 } from "node:path";
|
|
424
|
+
import { homedir as homedir2 } from "node:os";
|
|
425
|
+
import { randomUUID } from "node:crypto";
|
|
426
|
+
function imageVersion(bytes) {
|
|
427
|
+
let width = 0, height = 0;
|
|
428
|
+
if (bytes.length >= 30 && bytes.toString("ascii", 0, 4) === "RIFF" && bytes.toString("ascii", 8, 12) === "WEBP") {
|
|
429
|
+
const kind = bytes.toString("ascii", 12, 16);
|
|
430
|
+
if (kind === "VP8X") {
|
|
431
|
+
width = 1 + bytes.readUIntLE(24, 3);
|
|
432
|
+
height = 1 + bytes.readUIntLE(27, 3);
|
|
433
|
+
} else if (kind === "VP8L" && bytes[20] === 47) {
|
|
434
|
+
const bits = bytes.readUInt32LE(21);
|
|
435
|
+
width = (bits & 16383) + 1;
|
|
436
|
+
height = (bits >>> 14 & 16383) + 1;
|
|
437
|
+
} else if (kind === "VP8 ") {
|
|
438
|
+
width = bytes.readUInt16LE(26) & 16383;
|
|
439
|
+
height = bytes.readUInt16LE(28) & 16383;
|
|
440
|
+
}
|
|
441
|
+
} else if (bytes.length >= 24 && bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) {
|
|
442
|
+
width = bytes.readUInt32BE(16);
|
|
443
|
+
height = bytes.readUInt32BE(20);
|
|
444
|
+
}
|
|
445
|
+
if (width !== 1536 || ![1872, 2288].includes(height)) throw new Error(`\u56FE\u96C6\u5C3A\u5BF8 ${width}\xD7${height} \u4E0D\u7B26\u5408 Codex 8 \u5217\u534F\u8BAE`);
|
|
446
|
+
return height === 2288 ? 2 : 1;
|
|
447
|
+
}
|
|
448
|
+
async function confined(root, file) {
|
|
449
|
+
const base = await realpath(root), target = await realpath(resolve2(root, file));
|
|
450
|
+
const rel = relative(base, target);
|
|
451
|
+
if (rel.startsWith("..") || isAbsolute2(rel)) throw new Error("\u6587\u4EF6\u8D85\u51FA\u5BA0\u7269\u76EE\u5F55");
|
|
452
|
+
return target;
|
|
453
|
+
}
|
|
454
|
+
async function smallRead(path, limit) {
|
|
455
|
+
const info = await stat(path);
|
|
456
|
+
if (!info.isFile() || info.size > limit) throw new Error("\u6587\u4EF6\u8FC7\u5927\u6216\u4E0D\u662F\u666E\u901A\u6587\u4EF6");
|
|
457
|
+
return readFile2(path);
|
|
458
|
+
}
|
|
459
|
+
var PetLibrary = class {
|
|
460
|
+
constructor(assetRoot, dataRoot = join(process.env.DSH_HOME ?? join(homedir2(), ".dsh"), "codex-pet"), skillRoot = resolve2(assetRoot, "..", "..", "skills")) {
|
|
461
|
+
this.assetRoot = assetRoot;
|
|
462
|
+
this.dataRoot = dataRoot;
|
|
463
|
+
this.skillRoot = skillRoot;
|
|
464
|
+
this.customPath = join(dataRoot, "pets");
|
|
465
|
+
this.skillPath = join(skillRoot, "hatch-pet", "SKILL.md");
|
|
466
|
+
this.statePath = join(dataRoot, "config.json");
|
|
467
|
+
}
|
|
468
|
+
customPath;
|
|
469
|
+
skillPath;
|
|
470
|
+
statePath;
|
|
471
|
+
config = DEFAULT_CONFIG;
|
|
472
|
+
pets = [];
|
|
473
|
+
warnings = [];
|
|
474
|
+
configWarnings = [];
|
|
475
|
+
files = /* @__PURE__ */ new Map();
|
|
476
|
+
queue = Promise.resolve();
|
|
477
|
+
async init() {
|
|
478
|
+
try {
|
|
479
|
+
this.config = normalizeConfig(JSON.parse(await readFile2(this.statePath, "utf8")));
|
|
480
|
+
} catch (error) {
|
|
481
|
+
if (error.code !== "ENOENT") this.configWarnings.push("\u914D\u7F6E\u8BFB\u53D6\u5931\u8D25\uFF0C\u6682\u7528\u9ED8\u8BA4\u503C\uFF1B\u539F\u6587\u4EF6\u672A\u6539\u52A8\u3002");
|
|
482
|
+
}
|
|
483
|
+
await mkdir(this.customPath, { recursive: true });
|
|
484
|
+
await this.refresh();
|
|
485
|
+
}
|
|
486
|
+
async refresh() {
|
|
487
|
+
const pets = [];
|
|
488
|
+
const files = /* @__PURE__ */ new Map();
|
|
489
|
+
const warnings = [];
|
|
490
|
+
const add = async (id, name2, description, root, asset, source) => {
|
|
491
|
+
const file = await confined(root, asset);
|
|
492
|
+
const version = imageVersion(await smallRead(file, 32 * 1024 * 1024));
|
|
493
|
+
pets.push({ id, name: name2, description, version, source, url: `${BASE}/asset/${encodeURIComponent(id)}` });
|
|
494
|
+
files.set(id, { root, relative: asset });
|
|
495
|
+
};
|
|
496
|
+
for (const [id, name2, description] of BUILTINS) {
|
|
497
|
+
try {
|
|
498
|
+
await add(id, name2, description, this.assetRoot, `${id}/spritesheet.webp`, "builtin");
|
|
499
|
+
} catch {
|
|
500
|
+
warnings.push(`\u5185\u7F6E\u5BA0\u7269 ${name2} \u7684\u539F\u59CB\u56FE\u96C6\u7F3A\u5931\u6216\u4E0D\u517C\u5BB9\u3002`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
let folders = [];
|
|
504
|
+
try {
|
|
505
|
+
folders = await readdir(this.customPath);
|
|
506
|
+
} catch (error) {
|
|
507
|
+
if (error.code !== "ENOENT") warnings.push("\u65E0\u6CD5\u8BFB\u53D6\u81EA\u5B9A\u4E49\u5BA0\u7269\u76EE\u5F55\u3002");
|
|
508
|
+
}
|
|
509
|
+
for (const folder of folders.sort()) {
|
|
510
|
+
if (!/^[\w-]+$/.test(folder)) continue;
|
|
511
|
+
try {
|
|
512
|
+
const root = await confined(this.customPath, folder);
|
|
513
|
+
const manifest = JSON.parse((await smallRead(await confined(root, "pet.json"), 64 * 1024)).toString("utf8"));
|
|
514
|
+
if (typeof manifest.spritesheetPath !== "string") throw new Error("\u7F3A\u5C11\u56FE\u96C6\u8DEF\u5F84");
|
|
515
|
+
await add(`custom:${folder}`, String(manifest.displayName ?? folder).slice(0, 100), String(manifest.description ?? "").slice(0, 300), root, manifest.spritesheetPath, "custom");
|
|
516
|
+
} catch {
|
|
517
|
+
warnings.push(`\u81EA\u5B9A\u4E49\u5BA0\u7269 ${folder} \u672A\u901A\u8FC7\u683C\u5F0F\u6821\u9A8C\u3002`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
this.pets = pets;
|
|
521
|
+
this.files = files;
|
|
522
|
+
this.warnings = [...this.configWarnings, ...warnings];
|
|
523
|
+
}
|
|
524
|
+
async asset(id) {
|
|
525
|
+
const entry = this.files.get(id);
|
|
526
|
+
if (!entry) throw new Error("\u5BA0\u7269\u4E0D\u5B58\u5728");
|
|
527
|
+
return smallRead(await confined(entry.root, entry.relative), 32 * 1024 * 1024);
|
|
528
|
+
}
|
|
529
|
+
async update(value) {
|
|
530
|
+
const operation = this.queue.then(async () => {
|
|
531
|
+
const config = normalizeConfig(value, this.config);
|
|
532
|
+
if (!this.pets.some((pet) => pet.id === config.selected)) throw new Error("\u8BF7\u9009\u62E9\u5DF2\u52A0\u8F7D\u7684\u5BA0\u7269");
|
|
533
|
+
await mkdir(dirname2(this.statePath), { recursive: true });
|
|
534
|
+
const temp = `${this.statePath}.${randomUUID()}.tmp`;
|
|
535
|
+
try {
|
|
536
|
+
await writeFile(temp, JSON.stringify(config, null, 2), { encoding: "utf8", flag: "wx" });
|
|
537
|
+
await rename(temp, this.statePath);
|
|
538
|
+
} finally {
|
|
539
|
+
await unlink(temp).catch((error) => {
|
|
540
|
+
if (error.code !== "ENOENT") throw error;
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
this.config = config;
|
|
544
|
+
return config;
|
|
545
|
+
});
|
|
546
|
+
this.queue = operation.catch(() => void 0);
|
|
547
|
+
return operation;
|
|
548
|
+
}
|
|
549
|
+
snapshot() {
|
|
550
|
+
return { pets: this.pets, config: this.config, customPath: this.customPath, warnings: this.warnings, skillAvailable: existsSync2(this.skillPath), skillPath: this.skillPath };
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
// src/index.ts
|
|
555
|
+
var name = "michengai-codex-pet";
|
|
556
|
+
var inject = ["webServer"];
|
|
557
|
+
var packageRoot = fileURLToPath2(new URL("../", import.meta.url));
|
|
558
|
+
function json2(res, status2, data) {
|
|
559
|
+
res.writeHead(status2, {
|
|
560
|
+
"content-type": "application/json; charset=utf-8",
|
|
561
|
+
"cache-control": "no-store",
|
|
562
|
+
"x-content-type-options": "nosniff"
|
|
563
|
+
});
|
|
564
|
+
res.end(JSON.stringify(data));
|
|
565
|
+
}
|
|
566
|
+
function trustedWrite(req) {
|
|
567
|
+
if (req.headers["x-dsh-pet"] !== "1" || !req.headers["content-type"]?.startsWith("application/json"))
|
|
568
|
+
return false;
|
|
569
|
+
if (req.headers["sec-fetch-site"] === "cross-site") return false;
|
|
570
|
+
if (req.headers.origin) {
|
|
571
|
+
try {
|
|
572
|
+
if (new URL(req.headers.origin).host !== req.headers.host)
|
|
573
|
+
return false;
|
|
574
|
+
} catch {
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return true;
|
|
579
|
+
}
|
|
580
|
+
async function body(req) {
|
|
581
|
+
let size = 0;
|
|
582
|
+
const chunks = [];
|
|
583
|
+
for await (const chunk of req) {
|
|
584
|
+
size += chunk.length;
|
|
585
|
+
if (size > 16384) throw new Error("\u8BF7\u6C42\u8FC7\u5927");
|
|
586
|
+
chunks.push(Buffer.from(chunk));
|
|
587
|
+
}
|
|
588
|
+
const value = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
589
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
590
|
+
throw new Error("\u8BF7\u6C42\u5FC5\u987B\u662F JSON \u5BF9\u8C61");
|
|
591
|
+
return value;
|
|
592
|
+
}
|
|
593
|
+
async function createHost(options = {}) {
|
|
594
|
+
const root = options.root ?? packageRoot;
|
|
595
|
+
const library = new PetLibrary(
|
|
596
|
+
join2(root, "assets", "codex"),
|
|
597
|
+
options.dataRoot,
|
|
598
|
+
options.skillRoot
|
|
599
|
+
);
|
|
600
|
+
await library.init();
|
|
601
|
+
let updateHandler;
|
|
602
|
+
registerPluginUpdater(
|
|
603
|
+
{
|
|
604
|
+
logger: {
|
|
605
|
+
warn: (message) => console.warn("[dsh-codex-pet]", message)
|
|
606
|
+
},
|
|
607
|
+
webServer: {
|
|
608
|
+
register(route) {
|
|
609
|
+
updateHandler = route.handler;
|
|
610
|
+
return () => {
|
|
611
|
+
updateHandler = void 0;
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
endpoint: `${BASE}/api/update`,
|
|
618
|
+
packageName: "@michengai/dsh-codex-pet",
|
|
619
|
+
manifestUrl: new URL("../package.json", import.meta.url)
|
|
620
|
+
}
|
|
621
|
+
);
|
|
622
|
+
const snapshot = () => ({
|
|
623
|
+
...library.snapshot(),
|
|
624
|
+
creationAvailable: false,
|
|
625
|
+
creation: null
|
|
626
|
+
});
|
|
627
|
+
const handler = async (req, res) => {
|
|
628
|
+
try {
|
|
629
|
+
const authority = new URL(`http://${req.headers.host ?? ""}`);
|
|
630
|
+
if (!["localhost", "127.0.0.1", "[::1]"].includes(
|
|
631
|
+
authority.hostname
|
|
632
|
+
)) {
|
|
633
|
+
json2(res, 403, { error: "\u4E0D\u53D7\u4FE1\u4EFB\u7684 Host" });
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
const path = new URL(req.url ?? "/", authority).pathname;
|
|
637
|
+
if (path === `${BASE}/api/update` && updateHandler) {
|
|
638
|
+
await updateHandler(req, res);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (req.method === "GET" && path === `${BASE}/api/state`) {
|
|
642
|
+
json2(res, 200, snapshot());
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
if (req.method === "GET" && path.startsWith(`${BASE}/asset/`)) {
|
|
646
|
+
const bytes = await library.asset(
|
|
647
|
+
decodeURIComponent(path.slice(`${BASE}/asset/`.length))
|
|
648
|
+
);
|
|
649
|
+
const png = bytes[0] === 137;
|
|
650
|
+
res.writeHead(200, {
|
|
651
|
+
"content-type": png ? "image/png" : "image/webp",
|
|
652
|
+
"cache-control": "no-cache",
|
|
653
|
+
"x-content-type-options": "nosniff"
|
|
654
|
+
});
|
|
655
|
+
res.end(bytes);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
if (!path.startsWith(`${BASE}/api/`)) {
|
|
659
|
+
json2(res, 404, { error: "\u672A\u627E\u5230\u8D44\u6E90" });
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
if (req.method !== "POST") {
|
|
663
|
+
json2(res, 405, { error: "\u65B9\u6CD5\u4E0D\u652F\u6301" });
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
if (!trustedWrite(req)) {
|
|
667
|
+
json2(res, 403, { error: "\u8BF7\u6C42\u6765\u6E90\u6821\u9A8C\u5931\u8D25" });
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
const value = await body(req);
|
|
671
|
+
if (path === `${BASE}/api/config`) await library.update(value);
|
|
672
|
+
else if (path === `${BASE}/api/refresh`) await library.refresh();
|
|
673
|
+
else if (path === `${BASE}/api/create`) {
|
|
674
|
+
throw new Error("\u8BF7\u4ECE DSH \u5BA0\u7269\u8BBE\u7F6E\u53D1\u8D77\u521B\u5EFA\u4F1A\u8BDD");
|
|
675
|
+
} else if (path === `${BASE}/api/open-folder`) {
|
|
676
|
+
if (req.socket.remoteAddress && !["127.0.0.1", "::1", "::ffff:127.0.0.1"].includes(
|
|
677
|
+
req.socket.remoteAddress
|
|
678
|
+
))
|
|
679
|
+
throw new Error("\u53EA\u80FD\u5728\u8FD0\u884C DSH \u7684\u672C\u673A\u6253\u5F00\u6587\u4EF6\u5939");
|
|
680
|
+
await mkdir2(library.customPath, { recursive: true });
|
|
681
|
+
if (process.platform === "win32") {
|
|
682
|
+
const powershell = join2(
|
|
683
|
+
process.env.SystemRoot ?? "C:\\Windows",
|
|
684
|
+
"System32",
|
|
685
|
+
"WindowsPowerShell",
|
|
686
|
+
"v1.0",
|
|
687
|
+
"powershell.exe"
|
|
688
|
+
);
|
|
689
|
+
const script = `$ErrorActionPreference = "Stop"; Start-Process -FilePath explorer.exe -ArgumentList ('"' + $env:DSH_PET_OPEN_DIRECTORY + '"') -WindowStyle Normal`;
|
|
690
|
+
await new Promise((resolve3, reject) => {
|
|
691
|
+
execFile(
|
|
692
|
+
powershell,
|
|
693
|
+
[
|
|
694
|
+
"-NoProfile",
|
|
695
|
+
"-NonInteractive",
|
|
696
|
+
"-Command",
|
|
697
|
+
script
|
|
698
|
+
],
|
|
699
|
+
{
|
|
700
|
+
windowsHide: true,
|
|
701
|
+
timeout: 1e4,
|
|
702
|
+
env: {
|
|
703
|
+
...process.env,
|
|
704
|
+
DSH_PET_OPEN_DIRECTORY: library.customPath
|
|
705
|
+
}
|
|
706
|
+
},
|
|
707
|
+
(error) => error ? reject(
|
|
708
|
+
new Error(
|
|
709
|
+
`\u6253\u5F00\u6587\u4EF6\u5939\u5931\u8D25\uFF1A${error.message}`
|
|
710
|
+
)
|
|
711
|
+
) : resolve3()
|
|
712
|
+
);
|
|
713
|
+
});
|
|
714
|
+
} else {
|
|
715
|
+
const command = process.platform === "darwin" ? "open" : "xdg-open";
|
|
716
|
+
await new Promise((resolve3, reject) => {
|
|
717
|
+
const child = spawn2(command, [library.customPath], {
|
|
718
|
+
shell: false,
|
|
719
|
+
stdio: "ignore"
|
|
720
|
+
});
|
|
721
|
+
child.once("error", reject);
|
|
722
|
+
child.once("spawn", () => {
|
|
723
|
+
child.unref();
|
|
724
|
+
resolve3();
|
|
725
|
+
});
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
} else {
|
|
729
|
+
json2(res, 404, { error: "\u672A\u77E5\u64CD\u4F5C" });
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
json2(res, 200, snapshot());
|
|
733
|
+
} catch (error) {
|
|
734
|
+
if (res.headersSent) res.end();
|
|
735
|
+
else
|
|
736
|
+
json2(res, 400, {
|
|
737
|
+
error: error instanceof Error ? error.message : "\u64CD\u4F5C\u5931\u8D25"
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
return { handler, library, dispose: () => {
|
|
742
|
+
} };
|
|
743
|
+
}
|
|
744
|
+
function apply(ctx) {
|
|
745
|
+
ctx.effect(() => {
|
|
746
|
+
let disposed = false, remove, host;
|
|
747
|
+
void createHost().then((value) => {
|
|
748
|
+
host = value;
|
|
749
|
+
if (disposed) {
|
|
750
|
+
host.dispose();
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
remove = ctx.get("webServer").register({
|
|
754
|
+
kind: "prefix",
|
|
755
|
+
path: BASE,
|
|
756
|
+
handler: (req, res) => {
|
|
757
|
+
void value.handler(req, res);
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
}).catch(
|
|
761
|
+
(error) => console.error("[dsh-codex-pet] \u521D\u59CB\u5316\u5931\u8D25", error)
|
|
762
|
+
);
|
|
763
|
+
return () => {
|
|
764
|
+
disposed = true;
|
|
765
|
+
remove?.();
|
|
766
|
+
host?.dispose();
|
|
767
|
+
};
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
export {
|
|
771
|
+
apply,
|
|
772
|
+
createHost,
|
|
773
|
+
inject,
|
|
774
|
+
json2 as json,
|
|
775
|
+
name,
|
|
776
|
+
trustedWrite
|
|
777
|
+
};
|