@houser/edus-plugin 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -0
- package/cordis.yml +5 -0
- package/lib/client.js +7447 -0
- package/lib/index.js +718 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +753 -0
- package/lib/typert.remote-client.d.ts +50 -0
- package/lib/typert.remote-client.js +747 -0
- package/lib/types/auth.d.ts +28 -0
- package/lib/types/client/HtmlPublishPreview.d.ts +7 -0
- package/lib/types/client/HtmlWorkbench.d.ts +46 -0
- package/lib/types/client/MyWorksAction.d.ts +7 -0
- package/lib/types/client/html-publish.d.ts +21 -0
- package/lib/types/client/index.d.ts +10 -0
- package/lib/types/index.d.ts +13 -0
- package/lib/types/invariant.d.ts +5 -0
- package/lib/types/marker.d.ts +13 -0
- package/lib/types/publish-image-tool.d.ts +4 -0
- package/lib/types/types.d.ts +52 -0
- package/lib/types/webpage-command.d.ts +4 -0
- package/lib/types/webpage-instruction.d.ts +2 -0
- package/package.json +167 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { basename, extname } from "node:path";
|
|
3
|
+
import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
4
|
+
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
5
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
6
|
+
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
7
|
+
//#region lib/types/marker.js
|
|
8
|
+
const MARKER = /<!--\s*edus-publish:(.*?)-->\s*/gs;
|
|
9
|
+
function extractMarker(content) {
|
|
10
|
+
MARKER.lastIndex = 0;
|
|
11
|
+
const matched = MARKER.exec(content);
|
|
12
|
+
if (matched?.[1] === void 0) return null;
|
|
13
|
+
try {
|
|
14
|
+
const value = JSON.parse(matched[1]);
|
|
15
|
+
if (typeof value.id !== "string") return null;
|
|
16
|
+
return {
|
|
17
|
+
id: value.id,
|
|
18
|
+
...typeof value.sharePath === "string" ? { sharePath: value.sharePath } : {},
|
|
19
|
+
...typeof value.hash === "string" ? { hash: value.hash } : {},
|
|
20
|
+
...typeof value.owner === "string" ? { owner: value.owner } : {}
|
|
21
|
+
};
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function stripMarker(content) {
|
|
27
|
+
return content.replace(MARKER, "");
|
|
28
|
+
}
|
|
29
|
+
function canonicalizeBody(content) {
|
|
30
|
+
return `${stripMarker(content).replace(/\n+$/, "")}\n`;
|
|
31
|
+
}
|
|
32
|
+
function withMarker(body, marker) {
|
|
33
|
+
return `${canonicalizeBody(body)}<!-- edus-publish:${JSON.stringify(marker)} -->\n`;
|
|
34
|
+
}
|
|
35
|
+
async function sha256Hex(value) {
|
|
36
|
+
const bytes = new TextEncoder().encode(value);
|
|
37
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
38
|
+
return Array.from(new Uint8Array(digest), (value) => value.toString(16).padStart(2, "0")).join("");
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region lib/types/auth.js
|
|
42
|
+
/** Host-only ai-edu bridge. File methods are always fenced to the caller's session cwd. */
|
|
43
|
+
var __runInitializers = function(thisArg, initializers, value) {
|
|
44
|
+
var useValue = arguments.length > 2;
|
|
45
|
+
for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
46
|
+
return useValue ? value : void 0;
|
|
47
|
+
};
|
|
48
|
+
var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
49
|
+
function accept(f) {
|
|
50
|
+
if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
|
|
51
|
+
return f;
|
|
52
|
+
}
|
|
53
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
54
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
55
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
56
|
+
var _, done = false;
|
|
57
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
58
|
+
var context = {};
|
|
59
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
60
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
61
|
+
context.addInitializer = function(f) {
|
|
62
|
+
if (done) throw new TypeError("Cannot add initializers after decoration has completed");
|
|
63
|
+
extraInitializers.push(accept(f || null));
|
|
64
|
+
};
|
|
65
|
+
var result = (0, decorators[i])(kind === "accessor" ? {
|
|
66
|
+
get: descriptor.get,
|
|
67
|
+
set: descriptor.set
|
|
68
|
+
} : descriptor[key], context);
|
|
69
|
+
if (kind === "accessor") {
|
|
70
|
+
if (result === void 0) continue;
|
|
71
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
72
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
73
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
74
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
75
|
+
} else if (_ = accept(result)) if (kind === "field") initializers.unshift(_);
|
|
76
|
+
else descriptor[key] = _;
|
|
77
|
+
}
|
|
78
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
79
|
+
done = true;
|
|
80
|
+
};
|
|
81
|
+
const MAX_FILES = 300;
|
|
82
|
+
const MAX_DIRS = 500;
|
|
83
|
+
const INLINE_IMAGE = /data:(image\/[a-z0-9.+-]+);base64,([A-Z0-9+/=]+)/gi;
|
|
84
|
+
const LOCAL_REF = /(?:src|href)\s*=\s*["'](?!https?:|data:|\/\/|#|mailto:)([^"']+)["']/gi;
|
|
85
|
+
function htmlPath(value) {
|
|
86
|
+
return [".html", ".htm"].includes(extname(value).toLowerCase());
|
|
87
|
+
}
|
|
88
|
+
function login(wire) {
|
|
89
|
+
return {
|
|
90
|
+
id: wire.id,
|
|
91
|
+
phone: wire.phone,
|
|
92
|
+
token: wire.token ?? "",
|
|
93
|
+
isAdmin: wire.is_admin ?? false,
|
|
94
|
+
isVip: wire.is_vip ?? false,
|
|
95
|
+
title: wire.title,
|
|
96
|
+
vipLevel: wire.vip_level
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
async function publicCall(base, path, body) {
|
|
100
|
+
const response = await fetch(`${base}${path}`, {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: { "content-type": "application/json" },
|
|
103
|
+
body: JSON.stringify(body)
|
|
104
|
+
});
|
|
105
|
+
if (!response.ok) throw new Error(`ai-edu 请求失败:HTTP ${response.status}`);
|
|
106
|
+
const data = await response.json();
|
|
107
|
+
if (data.code !== 0) throw new Error(data.msg);
|
|
108
|
+
return data.data;
|
|
109
|
+
}
|
|
110
|
+
async function protectedCall(base, token, path, init) {
|
|
111
|
+
const response = await fetch(`${base}${path}`, {
|
|
112
|
+
...init,
|
|
113
|
+
headers: {
|
|
114
|
+
...init.headers,
|
|
115
|
+
authorization: `Bearer ${token}`
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
if (!response.ok) throw new Error(`ai-edu 请求失败:HTTP ${response.status}`);
|
|
119
|
+
const data = await response.json();
|
|
120
|
+
if (data.code !== 0) throw new Error(data.msg);
|
|
121
|
+
return data.data;
|
|
122
|
+
}
|
|
123
|
+
function assertSelfContained(body) {
|
|
124
|
+
LOCAL_REF.lastIndex = 0;
|
|
125
|
+
const references = [...body.matchAll(LOCAL_REF)].map((match) => match[1]).filter((item) => item !== void 0);
|
|
126
|
+
if (references.length > 0) throw new Error(`页面引用了本地资源:${[...new Set(references)].slice(0, 5).join("、")}。请将样式和脚本内联,并使用图片绝对 URL。`);
|
|
127
|
+
}
|
|
128
|
+
async function uploadInlineImages(base, content) {
|
|
129
|
+
INLINE_IMAGE.lastIndex = 0;
|
|
130
|
+
const unique = /* @__PURE__ */ new Map();
|
|
131
|
+
for (const [all, mediaType, payload] of content.matchAll(INLINE_IMAGE)) {
|
|
132
|
+
if (mediaType === void 0 || payload === void 0 || unique.has(all)) continue;
|
|
133
|
+
const bytes = Buffer.from(payload, "base64");
|
|
134
|
+
if (bytes.byteLength > 10 * 1024 * 1024) throw new Error("页面内嵌图片超过 10MB 上限");
|
|
135
|
+
unique.set(all, (async () => {
|
|
136
|
+
const form = new FormData();
|
|
137
|
+
form.append("file", new Blob([new Uint8Array(bytes)], { type: mediaType }), `${(await sha256Hex(payload)).slice(0, 16)}.${mediaType.split("/")[1] ?? "png"}`);
|
|
138
|
+
const response = await fetch(`${base}/api/common/upload`, {
|
|
139
|
+
method: "POST",
|
|
140
|
+
body: form
|
|
141
|
+
});
|
|
142
|
+
if (!response.ok) throw new Error(`图片上传失败:HTTP ${response.status}`);
|
|
143
|
+
const wire = await response.json();
|
|
144
|
+
if (typeof wire.url !== "string" || wire.url === "") throw new Error("图片上传未返回 URL");
|
|
145
|
+
return wire.url;
|
|
146
|
+
})());
|
|
147
|
+
}
|
|
148
|
+
const urls = new Map(await Promise.all([...unique.entries()].map(async ([source, upload]) => [source, await upload])));
|
|
149
|
+
return content.replace(INLINE_IMAGE, (source) => urls.get(source) ?? source);
|
|
150
|
+
}
|
|
151
|
+
let EdusAuthService = (() => {
|
|
152
|
+
let _classSuper = TypertRemoteService;
|
|
153
|
+
let _instanceExtraInitializers = [];
|
|
154
|
+
let _shareOrigin_decorators;
|
|
155
|
+
let _login_decorators;
|
|
156
|
+
let _loginInfo_decorators;
|
|
157
|
+
let _sendPhoneCode_decorators;
|
|
158
|
+
let _loginByPhone_decorators;
|
|
159
|
+
let _htmlList_decorators;
|
|
160
|
+
let _htmlShare_decorators;
|
|
161
|
+
let _listHtmlFiles_decorators;
|
|
162
|
+
let _readHtmlFile_decorators;
|
|
163
|
+
let _writeHtmlFile_decorators;
|
|
164
|
+
let _publishFile_decorators;
|
|
165
|
+
let _htmlVersions_decorators;
|
|
166
|
+
let _htmlVersionDetail_decorators;
|
|
167
|
+
let _applyVersion_decorators;
|
|
168
|
+
let _htmlVersionDelete_decorators;
|
|
169
|
+
return class EdusAuthService extends _classSuper {
|
|
170
|
+
static {
|
|
171
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
172
|
+
_shareOrigin_decorators = [Remote("shareOrigin")];
|
|
173
|
+
_login_decorators = [Remote("login")];
|
|
174
|
+
_loginInfo_decorators = [Remote("loginInfo")];
|
|
175
|
+
_sendPhoneCode_decorators = [Remote("sendPhoneCode")];
|
|
176
|
+
_loginByPhone_decorators = [Remote("loginByPhone")];
|
|
177
|
+
_htmlList_decorators = [Remote("htmlList")];
|
|
178
|
+
_htmlShare_decorators = [Remote("htmlShare")];
|
|
179
|
+
_listHtmlFiles_decorators = [Remote("listHtmlFiles")];
|
|
180
|
+
_readHtmlFile_decorators = [Remote("readHtmlFile")];
|
|
181
|
+
_writeHtmlFile_decorators = [Remote("writeHtmlFile")];
|
|
182
|
+
_publishFile_decorators = [Remote("publishFile")];
|
|
183
|
+
_htmlVersions_decorators = [Remote("htmlVersions")];
|
|
184
|
+
_htmlVersionDetail_decorators = [Remote("htmlVersionDetail")];
|
|
185
|
+
_applyVersion_decorators = [Remote("applyVersion")];
|
|
186
|
+
_htmlVersionDelete_decorators = [Remote("htmlVersionDelete")];
|
|
187
|
+
__esDecorate(this, null, _shareOrigin_decorators, {
|
|
188
|
+
kind: "method",
|
|
189
|
+
name: "shareOrigin",
|
|
190
|
+
static: false,
|
|
191
|
+
private: false,
|
|
192
|
+
access: {
|
|
193
|
+
has: (obj) => "shareOrigin" in obj,
|
|
194
|
+
get: (obj) => obj.shareOrigin
|
|
195
|
+
},
|
|
196
|
+
metadata: _metadata
|
|
197
|
+
}, null, _instanceExtraInitializers);
|
|
198
|
+
__esDecorate(this, null, _login_decorators, {
|
|
199
|
+
kind: "method",
|
|
200
|
+
name: "login",
|
|
201
|
+
static: false,
|
|
202
|
+
private: false,
|
|
203
|
+
access: {
|
|
204
|
+
has: (obj) => "login" in obj,
|
|
205
|
+
get: (obj) => obj.login
|
|
206
|
+
},
|
|
207
|
+
metadata: _metadata
|
|
208
|
+
}, null, _instanceExtraInitializers);
|
|
209
|
+
__esDecorate(this, null, _loginInfo_decorators, {
|
|
210
|
+
kind: "method",
|
|
211
|
+
name: "loginInfo",
|
|
212
|
+
static: false,
|
|
213
|
+
private: false,
|
|
214
|
+
access: {
|
|
215
|
+
has: (obj) => "loginInfo" in obj,
|
|
216
|
+
get: (obj) => obj.loginInfo
|
|
217
|
+
},
|
|
218
|
+
metadata: _metadata
|
|
219
|
+
}, null, _instanceExtraInitializers);
|
|
220
|
+
__esDecorate(this, null, _sendPhoneCode_decorators, {
|
|
221
|
+
kind: "method",
|
|
222
|
+
name: "sendPhoneCode",
|
|
223
|
+
static: false,
|
|
224
|
+
private: false,
|
|
225
|
+
access: {
|
|
226
|
+
has: (obj) => "sendPhoneCode" in obj,
|
|
227
|
+
get: (obj) => obj.sendPhoneCode
|
|
228
|
+
},
|
|
229
|
+
metadata: _metadata
|
|
230
|
+
}, null, _instanceExtraInitializers);
|
|
231
|
+
__esDecorate(this, null, _loginByPhone_decorators, {
|
|
232
|
+
kind: "method",
|
|
233
|
+
name: "loginByPhone",
|
|
234
|
+
static: false,
|
|
235
|
+
private: false,
|
|
236
|
+
access: {
|
|
237
|
+
has: (obj) => "loginByPhone" in obj,
|
|
238
|
+
get: (obj) => obj.loginByPhone
|
|
239
|
+
},
|
|
240
|
+
metadata: _metadata
|
|
241
|
+
}, null, _instanceExtraInitializers);
|
|
242
|
+
__esDecorate(this, null, _htmlList_decorators, {
|
|
243
|
+
kind: "method",
|
|
244
|
+
name: "htmlList",
|
|
245
|
+
static: false,
|
|
246
|
+
private: false,
|
|
247
|
+
access: {
|
|
248
|
+
has: (obj) => "htmlList" in obj,
|
|
249
|
+
get: (obj) => obj.htmlList
|
|
250
|
+
},
|
|
251
|
+
metadata: _metadata
|
|
252
|
+
}, null, _instanceExtraInitializers);
|
|
253
|
+
__esDecorate(this, null, _htmlShare_decorators, {
|
|
254
|
+
kind: "method",
|
|
255
|
+
name: "htmlShare",
|
|
256
|
+
static: false,
|
|
257
|
+
private: false,
|
|
258
|
+
access: {
|
|
259
|
+
has: (obj) => "htmlShare" in obj,
|
|
260
|
+
get: (obj) => obj.htmlShare
|
|
261
|
+
},
|
|
262
|
+
metadata: _metadata
|
|
263
|
+
}, null, _instanceExtraInitializers);
|
|
264
|
+
__esDecorate(this, null, _listHtmlFiles_decorators, {
|
|
265
|
+
kind: "method",
|
|
266
|
+
name: "listHtmlFiles",
|
|
267
|
+
static: false,
|
|
268
|
+
private: false,
|
|
269
|
+
access: {
|
|
270
|
+
has: (obj) => "listHtmlFiles" in obj,
|
|
271
|
+
get: (obj) => obj.listHtmlFiles
|
|
272
|
+
},
|
|
273
|
+
metadata: _metadata
|
|
274
|
+
}, null, _instanceExtraInitializers);
|
|
275
|
+
__esDecorate(this, null, _readHtmlFile_decorators, {
|
|
276
|
+
kind: "method",
|
|
277
|
+
name: "readHtmlFile",
|
|
278
|
+
static: false,
|
|
279
|
+
private: false,
|
|
280
|
+
access: {
|
|
281
|
+
has: (obj) => "readHtmlFile" in obj,
|
|
282
|
+
get: (obj) => obj.readHtmlFile
|
|
283
|
+
},
|
|
284
|
+
metadata: _metadata
|
|
285
|
+
}, null, _instanceExtraInitializers);
|
|
286
|
+
__esDecorate(this, null, _writeHtmlFile_decorators, {
|
|
287
|
+
kind: "method",
|
|
288
|
+
name: "writeHtmlFile",
|
|
289
|
+
static: false,
|
|
290
|
+
private: false,
|
|
291
|
+
access: {
|
|
292
|
+
has: (obj) => "writeHtmlFile" in obj,
|
|
293
|
+
get: (obj) => obj.writeHtmlFile
|
|
294
|
+
},
|
|
295
|
+
metadata: _metadata
|
|
296
|
+
}, null, _instanceExtraInitializers);
|
|
297
|
+
__esDecorate(this, null, _publishFile_decorators, {
|
|
298
|
+
kind: "method",
|
|
299
|
+
name: "publishFile",
|
|
300
|
+
static: false,
|
|
301
|
+
private: false,
|
|
302
|
+
access: {
|
|
303
|
+
has: (obj) => "publishFile" in obj,
|
|
304
|
+
get: (obj) => obj.publishFile
|
|
305
|
+
},
|
|
306
|
+
metadata: _metadata
|
|
307
|
+
}, null, _instanceExtraInitializers);
|
|
308
|
+
__esDecorate(this, null, _htmlVersions_decorators, {
|
|
309
|
+
kind: "method",
|
|
310
|
+
name: "htmlVersions",
|
|
311
|
+
static: false,
|
|
312
|
+
private: false,
|
|
313
|
+
access: {
|
|
314
|
+
has: (obj) => "htmlVersions" in obj,
|
|
315
|
+
get: (obj) => obj.htmlVersions
|
|
316
|
+
},
|
|
317
|
+
metadata: _metadata
|
|
318
|
+
}, null, _instanceExtraInitializers);
|
|
319
|
+
__esDecorate(this, null, _htmlVersionDetail_decorators, {
|
|
320
|
+
kind: "method",
|
|
321
|
+
name: "htmlVersionDetail",
|
|
322
|
+
static: false,
|
|
323
|
+
private: false,
|
|
324
|
+
access: {
|
|
325
|
+
has: (obj) => "htmlVersionDetail" in obj,
|
|
326
|
+
get: (obj) => obj.htmlVersionDetail
|
|
327
|
+
},
|
|
328
|
+
metadata: _metadata
|
|
329
|
+
}, null, _instanceExtraInitializers);
|
|
330
|
+
__esDecorate(this, null, _applyVersion_decorators, {
|
|
331
|
+
kind: "method",
|
|
332
|
+
name: "applyVersion",
|
|
333
|
+
static: false,
|
|
334
|
+
private: false,
|
|
335
|
+
access: {
|
|
336
|
+
has: (obj) => "applyVersion" in obj,
|
|
337
|
+
get: (obj) => obj.applyVersion
|
|
338
|
+
},
|
|
339
|
+
metadata: _metadata
|
|
340
|
+
}, null, _instanceExtraInitializers);
|
|
341
|
+
__esDecorate(this, null, _htmlVersionDelete_decorators, {
|
|
342
|
+
kind: "method",
|
|
343
|
+
name: "htmlVersionDelete",
|
|
344
|
+
static: false,
|
|
345
|
+
private: false,
|
|
346
|
+
access: {
|
|
347
|
+
has: (obj) => "htmlVersionDelete" in obj,
|
|
348
|
+
get: (obj) => obj.htmlVersionDelete
|
|
349
|
+
},
|
|
350
|
+
metadata: _metadata
|
|
351
|
+
}, null, _instanceExtraInitializers);
|
|
352
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, {
|
|
353
|
+
enumerable: true,
|
|
354
|
+
configurable: true,
|
|
355
|
+
writable: true,
|
|
356
|
+
value: _metadata
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
baseUrl = __runInitializers(this, _instanceExtraInitializers);
|
|
360
|
+
/** Session cwd and filesystem authority are mandatory for every file Remote call. */
|
|
361
|
+
static inject = ["sessions", "fs"];
|
|
362
|
+
constructor(ctx, baseUrl) {
|
|
363
|
+
super(ctx, "edus");
|
|
364
|
+
this.baseUrl = baseUrl;
|
|
365
|
+
}
|
|
366
|
+
sessionCwd(sessionId) {
|
|
367
|
+
const session = this.ctx.sessions.get(SessionId(sessionId));
|
|
368
|
+
if (session?.header.cwd === void 0) throw new Error("当前会话没有可用工作区");
|
|
369
|
+
return session.header.cwd;
|
|
370
|
+
}
|
|
371
|
+
async target(sessionId, path) {
|
|
372
|
+
if (!htmlPath(path)) throw new Error("只能操作 .html 或 .htm 文件");
|
|
373
|
+
const cwd = this.sessionCwd(sessionId);
|
|
374
|
+
const root = await this.ctx.fs.resolve(cwd);
|
|
375
|
+
if ((await this.ctx.fs.lstat(path, { cwd }))?.type === "symlink") throw new Error("不允许操作符号链接");
|
|
376
|
+
const target = await this.ctx.fs.resolve(path, { cwd });
|
|
377
|
+
if (!this.ctx.fs.contains(root, target)) throw new Error("文件必须位于当前会话工作区内");
|
|
378
|
+
if ((await this.ctx.fs.stat(target))?.type !== "file") throw new Error("目标不是普通文件");
|
|
379
|
+
return target;
|
|
380
|
+
}
|
|
381
|
+
policy(sessionId) {
|
|
382
|
+
const policy = this.ctx.get("sandboxPolicy");
|
|
383
|
+
const session = this.ctx.sessions.get(SessionId(sessionId));
|
|
384
|
+
return policy?.resolve(session === void 0 ? {} : { session });
|
|
385
|
+
}
|
|
386
|
+
shareOrigin() {
|
|
387
|
+
return Promise.resolve(this.baseUrl);
|
|
388
|
+
}
|
|
389
|
+
async login(phone, password) {
|
|
390
|
+
return login(await publicCall(this.baseUrl, "/api/login/login_by_password", {
|
|
391
|
+
phone,
|
|
392
|
+
password
|
|
393
|
+
}));
|
|
394
|
+
}
|
|
395
|
+
async loginInfo(token) {
|
|
396
|
+
return {
|
|
397
|
+
...login(await protectedCall(this.baseUrl, token, "/api/login/login_info", { method: "GET" })),
|
|
398
|
+
token
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
async sendPhoneCode(phone, captcha) {
|
|
402
|
+
await publicCall(this.baseUrl, "/api/login/send_phone_code", {
|
|
403
|
+
phone,
|
|
404
|
+
captcha
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
async loginByPhone(phone, code) {
|
|
408
|
+
return login(await publicCall(this.baseUrl, "/api/login/login_by_phone", {
|
|
409
|
+
phone,
|
|
410
|
+
code
|
|
411
|
+
}));
|
|
412
|
+
}
|
|
413
|
+
async htmlList(token, page, size, search) {
|
|
414
|
+
const query = new URLSearchParams({
|
|
415
|
+
page: String(page),
|
|
416
|
+
size: String(size)
|
|
417
|
+
});
|
|
418
|
+
if (search?.trim()) query.set("search", search.trim());
|
|
419
|
+
const result = await protectedCall(this.baseUrl, token, `/api/ai/html_list?${query}`, { method: "GET" });
|
|
420
|
+
return {
|
|
421
|
+
list: result.list.map((value) => ({
|
|
422
|
+
id: value.id,
|
|
423
|
+
title: value.title,
|
|
424
|
+
text: value.text,
|
|
425
|
+
images: value.images,
|
|
426
|
+
status: value.status,
|
|
427
|
+
views: value.views,
|
|
428
|
+
thumb: value.thumb,
|
|
429
|
+
from: value.from,
|
|
430
|
+
createdAt: value.created_at,
|
|
431
|
+
updatedAt: value.updated_at
|
|
432
|
+
})),
|
|
433
|
+
total: result.total,
|
|
434
|
+
page: result.page,
|
|
435
|
+
size: result.size
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
async htmlShare(token, id) {
|
|
439
|
+
const result = await protectedCall(this.baseUrl, token, `/api/ai/html_share?id=${encodeURIComponent(id)}`, { method: "GET" });
|
|
440
|
+
return `${this.baseUrl}${result.share_path}`;
|
|
441
|
+
}
|
|
442
|
+
async listHtmlFiles(sessionId) {
|
|
443
|
+
const cwd = this.sessionCwd(sessionId);
|
|
444
|
+
const root = await this.ctx.fs.resolve(cwd);
|
|
445
|
+
const files = [];
|
|
446
|
+
let dirs = 0;
|
|
447
|
+
let truncated = false;
|
|
448
|
+
const visit = async (directory, relative) => {
|
|
449
|
+
if (truncated || dirs++ >= MAX_DIRS) {
|
|
450
|
+
truncated = true;
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
for (const entry of await this.ctx.fs.listDir(directory)) {
|
|
454
|
+
if (truncated) return;
|
|
455
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === ".git") continue;
|
|
456
|
+
const path = relative === "" ? entry.name : `${relative}/${entry.name}`;
|
|
457
|
+
if (entry.type === "directory") await visit(entry.target, path);
|
|
458
|
+
if (entry.type === "file" && htmlPath(entry.name)) {
|
|
459
|
+
if (files.length >= MAX_FILES) {
|
|
460
|
+
truncated = true;
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
files.push({
|
|
464
|
+
path,
|
|
465
|
+
name: basename(path),
|
|
466
|
+
size: entry.size ?? 0,
|
|
467
|
+
modifiedAt: ""
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
await visit(root, "");
|
|
473
|
+
return {
|
|
474
|
+
files: files.sort((a, b) => a.path.localeCompare(b.path)),
|
|
475
|
+
truncated
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
async readHtmlFile(sessionId, path) {
|
|
479
|
+
return this.ctx.fs.readText(await this.target(sessionId, path));
|
|
480
|
+
}
|
|
481
|
+
async writeHtmlFile(sessionId, path, content) {
|
|
482
|
+
await this.ctx.fs.writeText(await this.target(sessionId, path), content, void 0, void 0, this.policy(sessionId));
|
|
483
|
+
}
|
|
484
|
+
async publishFile(token, sessionId, path, account, title, createNew) {
|
|
485
|
+
const publishAsNew = createNew ?? false;
|
|
486
|
+
const target = await this.target(sessionId, path);
|
|
487
|
+
const current = await this.ctx.fs.readText(target);
|
|
488
|
+
const previous = extractMarker(current);
|
|
489
|
+
const beforeImages = canonicalizeBody(current);
|
|
490
|
+
const body = canonicalizeBody(await uploadInlineImages(this.baseUrl, stripMarker(beforeImages)));
|
|
491
|
+
assertSelfContained(body);
|
|
492
|
+
const hash = await sha256Hex(body);
|
|
493
|
+
if (!publishAsNew && previous?.owner === account && previous.hash === hash) return {
|
|
494
|
+
id: previous.id,
|
|
495
|
+
content: null,
|
|
496
|
+
unchanged: true,
|
|
497
|
+
reassigned: false,
|
|
498
|
+
shareUrl: previous.sharePath === void 0 ? null : `${this.baseUrl}${previous.sharePath}`
|
|
499
|
+
};
|
|
500
|
+
const foreign = previous?.owner !== void 0 && previous.owner !== account;
|
|
501
|
+
const publishTitle = title?.trim() || basename(path);
|
|
502
|
+
const publish = async (id) => protectedCall(this.baseUrl, token, "/api/ai/html_publish", {
|
|
503
|
+
method: "POST",
|
|
504
|
+
headers: { "content-type": "application/json" },
|
|
505
|
+
body: JSON.stringify({
|
|
506
|
+
...id === void 0 ? {} : { id },
|
|
507
|
+
title: publishTitle,
|
|
508
|
+
html: body,
|
|
509
|
+
from: "edus"
|
|
510
|
+
})
|
|
511
|
+
});
|
|
512
|
+
let record;
|
|
513
|
+
let reassigned = foreign;
|
|
514
|
+
try {
|
|
515
|
+
record = await publish(publishAsNew || foreign ? void 0 : previous?.id);
|
|
516
|
+
} catch (error) {
|
|
517
|
+
if (previous?.owner !== void 0 || previous?.id === void 0) throw error;
|
|
518
|
+
record = await publish();
|
|
519
|
+
reassigned = true;
|
|
520
|
+
}
|
|
521
|
+
protectedCall(this.baseUrl, token, "/api/ai/html_thumb", {
|
|
522
|
+
method: "POST",
|
|
523
|
+
headers: { "content-type": "application/json" },
|
|
524
|
+
body: JSON.stringify({ id: record.id })
|
|
525
|
+
}).catch(() => void 0);
|
|
526
|
+
let sharePath = publishAsNew || reassigned || previous === null ? void 0 : previous?.sharePath;
|
|
527
|
+
if (sharePath === void 0) sharePath = (await protectedCall(this.baseUrl, token, `/api/ai/html_share?id=${encodeURIComponent(record.id)}`, { method: "GET" })).share_path;
|
|
528
|
+
const written = withMarker(body, {
|
|
529
|
+
id: record.id,
|
|
530
|
+
sharePath,
|
|
531
|
+
hash,
|
|
532
|
+
owner: account
|
|
533
|
+
});
|
|
534
|
+
await this.ctx.fs.writeText(target, written, void 0, void 0, this.policy(sessionId));
|
|
535
|
+
return {
|
|
536
|
+
id: record.id,
|
|
537
|
+
content: written,
|
|
538
|
+
unchanged: false,
|
|
539
|
+
reassigned,
|
|
540
|
+
shareUrl: `${this.baseUrl}${sharePath}`
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
async htmlVersions(token, htmlId) {
|
|
544
|
+
return (await protectedCall(this.baseUrl, token, `/api/ai/html_versions?html_id=${encodeURIComponent(htmlId)}`, { method: "GET" })).map((value) => ({
|
|
545
|
+
id: value.id,
|
|
546
|
+
status: value.status,
|
|
547
|
+
createdAt: value.created_at
|
|
548
|
+
}));
|
|
549
|
+
}
|
|
550
|
+
async htmlVersionDetail(token, id) {
|
|
551
|
+
return (await protectedCall(this.baseUrl, token, `/api/ai/html_version_detail?id=${encodeURIComponent(id)}`, { method: "GET" })).html;
|
|
552
|
+
}
|
|
553
|
+
async applyVersion(token, sessionId, path, id) {
|
|
554
|
+
const target = await this.target(sessionId, path);
|
|
555
|
+
const marker = extractMarker(await this.ctx.fs.readText(target));
|
|
556
|
+
const body = await this.htmlVersionDetail(token, id);
|
|
557
|
+
const written = marker === null ? body : withMarker(body, marker);
|
|
558
|
+
await this.ctx.fs.writeText(target, written, void 0, void 0, this.policy(sessionId));
|
|
559
|
+
return written;
|
|
560
|
+
}
|
|
561
|
+
async htmlVersionDelete(token, id) {
|
|
562
|
+
await protectedCall(this.baseUrl, token, "/api/ai/html_version_delete", {
|
|
563
|
+
method: "POST",
|
|
564
|
+
headers: { "content-type": "application/json" },
|
|
565
|
+
body: JSON.stringify({ id })
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
})();
|
|
570
|
+
//#endregion
|
|
571
|
+
//#region lib/types/publish-image-tool.js
|
|
572
|
+
function findImage(blocks, id) {
|
|
573
|
+
for (const block of blocks) {
|
|
574
|
+
if (block.type === "image" && String(block.attachment.attachmentId) === id) return block.attachment;
|
|
575
|
+
if (block.type === "tool-result") {
|
|
576
|
+
const found = findImage(block.content, id);
|
|
577
|
+
if (found !== void 0) return found;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
/** Publish only images already attached to the calling conversation. */
|
|
582
|
+
function applyPublishImageTool(ctx, baseUrl) {
|
|
583
|
+
ctx.inject(["tools"], (toolCtx) => {
|
|
584
|
+
toolCtx.tools.register(defineTool({
|
|
585
|
+
name: "publish_image",
|
|
586
|
+
description: "上传当前对话中用户明确要求使用的图片,返回用于 HTML 的公开绝对 URL。",
|
|
587
|
+
parameters: { attachmentId: {
|
|
588
|
+
type: "string",
|
|
589
|
+
required: true,
|
|
590
|
+
description: "对话中显示的 Image sha256:… id。"
|
|
591
|
+
} },
|
|
592
|
+
output: {
|
|
593
|
+
schema: {
|
|
594
|
+
type: "object",
|
|
595
|
+
additionalProperties: false,
|
|
596
|
+
properties: { url: {
|
|
597
|
+
type: "string",
|
|
598
|
+
required: true
|
|
599
|
+
} }
|
|
600
|
+
},
|
|
601
|
+
render: (_args, value) => [{
|
|
602
|
+
type: "text",
|
|
603
|
+
text: `Uploaded image: ${value.url}`
|
|
604
|
+
}]
|
|
605
|
+
},
|
|
606
|
+
async execute(args, execution) {
|
|
607
|
+
if (execution.agent === void 0) throw new Error("publish_image requires an agent session");
|
|
608
|
+
const attachment = execution.agent.session.deriveMessages().map((message) => findImage(message.content, args.attachmentId)).find(Boolean);
|
|
609
|
+
const store = ctx.get("attachments");
|
|
610
|
+
if (attachment === void 0 || store === void 0) throw new Error("该图片不在当前会话或附件服务不可用");
|
|
611
|
+
const image = await store.readImage(attachment);
|
|
612
|
+
const form = new FormData();
|
|
613
|
+
form.append("file", new Blob([new Uint8Array(image.data)], { type: attachment.mediaType }), attachment.name ?? "page-image.png");
|
|
614
|
+
const response = await fetch(`${baseUrl}/api/common/upload`, {
|
|
615
|
+
method: "POST",
|
|
616
|
+
body: form
|
|
617
|
+
});
|
|
618
|
+
if (!response.ok) throw new Error(`图片上传失败:HTTP ${response.status}`);
|
|
619
|
+
const data = await response.json();
|
|
620
|
+
if (typeof data.url !== "string" || data.url === "") throw new Error("图片上传未返回 URL");
|
|
621
|
+
return { url: data.url };
|
|
622
|
+
}
|
|
623
|
+
}));
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
//#endregion
|
|
627
|
+
//#region lib/types/webpage-instruction.js
|
|
628
|
+
const WEBPAGE_INSTRUCTION = `# 网页生成规则
|
|
629
|
+
|
|
630
|
+
当用户要求创建或修改网页时,必须遵守以下规则。
|
|
631
|
+
|
|
632
|
+
## 文件结构
|
|
633
|
+
|
|
634
|
+
- 最终网页必须是一个自包含的 \`.html\` 文件。
|
|
635
|
+
- 不得将 HTML、CSS、JavaScript 拆分为多个本地文件。
|
|
636
|
+
- CSS 必须写在 HTML 的 \`<style>\` 中。
|
|
637
|
+
- JavaScript 必须写在 HTML 的 \`<script>\` 中。
|
|
638
|
+
- 禁止引用 \`./xxx.css\`、\`./xxx.js\` 等本地相对资源。
|
|
639
|
+
|
|
640
|
+
## 图片和外部资源
|
|
641
|
+
|
|
642
|
+
- 禁止将图片编码为 \`data:\` URI。
|
|
643
|
+
- 用户明确要求使用当前对话中提供的图片时,必须调用 \`publish_image\` 获取可访问的绝对 URL,再将该 URL 写入 \`<img src="...">\` 等位置。
|
|
644
|
+
- 已有的外部图片绝对 URL 可以直接使用。
|
|
645
|
+
- 字体、图标等外部资源必须使用可公开访问的绝对 URL,例如 CDN。
|
|
646
|
+
- 不得引用工作区中的本地资源文件。
|
|
647
|
+
|
|
648
|
+
## 原因
|
|
649
|
+
|
|
650
|
+
发布系统以单个 HTML 字符串存储网页,并通过不存在同级资源目录的分享链接提供页面。因此,本地相对路径可能在工作区预览时正常,但发布后无法访问。
|
|
651
|
+
|
|
652
|
+
## 完成任务
|
|
653
|
+
|
|
654
|
+
网页完成后:
|
|
655
|
+
|
|
656
|
+
1. 将最终文件保存到当前工作区。
|
|
657
|
+
2. 文件扩展名必须为 \`.html\`。
|
|
658
|
+
3. 告知用户可以从文件侧边栏打开该 HTML。
|
|
659
|
+
4. 告知用户可以通过页面面板中的「发布」功能生成分享链接。
|
|
660
|
+
5. 不得自行发布网页,除非用户明确要求发布。`;
|
|
661
|
+
//#endregion
|
|
662
|
+
//#region lib/types/webpage-command.js
|
|
663
|
+
/** Register the user-facing slash command without making webpage rules optional. */
|
|
664
|
+
function applyWebpageCommand(ctx) {
|
|
665
|
+
ctx.inject(["commands"], (commandCtx) => {
|
|
666
|
+
commandCtx.commands.register({
|
|
667
|
+
name: "generate-webpage",
|
|
668
|
+
description: "制作网页:根据文字需求和配图生成网页",
|
|
669
|
+
input: {
|
|
670
|
+
hint: "[网页需求]",
|
|
671
|
+
attachments: true
|
|
672
|
+
},
|
|
673
|
+
recordInput: false,
|
|
674
|
+
handler: ({ agent, rawInput, attachments }) => {
|
|
675
|
+
const request = rawInput.trim();
|
|
676
|
+
if (request === "" && attachments.length === 0) return {
|
|
677
|
+
kind: "error",
|
|
678
|
+
text: "请在 /generate-webpage 后描述网页需求,或附上要使用的图片。"
|
|
679
|
+
};
|
|
680
|
+
agent.inject(createUserMessage({
|
|
681
|
+
content: [{
|
|
682
|
+
type: "text",
|
|
683
|
+
text: WEBPAGE_INSTRUCTION
|
|
684
|
+
}],
|
|
685
|
+
source: {
|
|
686
|
+
kind: "plugin",
|
|
687
|
+
plugin: "edus-plugin",
|
|
688
|
+
form: "instructions"
|
|
689
|
+
}
|
|
690
|
+
}));
|
|
691
|
+
agent.steer(createUserMessage({
|
|
692
|
+
content: [...attachments, {
|
|
693
|
+
type: "text",
|
|
694
|
+
text: request === "" ? "请根据用户提供的图片制作网页。" : request
|
|
695
|
+
}],
|
|
696
|
+
source: { kind: "user" }
|
|
697
|
+
}));
|
|
698
|
+
return {
|
|
699
|
+
kind: "success",
|
|
700
|
+
text: "已提交网页制作需求。"
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
//#endregion
|
|
707
|
+
//#region lib/types/index.js
|
|
708
|
+
const name = "edus-plugin";
|
|
709
|
+
const Config = z.object({ aiEduBaseUrl: z.string().required() });
|
|
710
|
+
const inject = [];
|
|
711
|
+
function apply(ctx, config) {
|
|
712
|
+
const baseUrl = config.aiEduBaseUrl.replace(/\/+$/, "");
|
|
713
|
+
ctx.plugin(EdusAuthService, baseUrl);
|
|
714
|
+
applyPublishImageTool(ctx, baseUrl);
|
|
715
|
+
applyWebpageCommand(ctx);
|
|
716
|
+
}
|
|
717
|
+
//#endregion
|
|
718
|
+
export { Config, EdusAuthService, apply, inject, name };
|